diff --git a/.github/workflows/xcode.yml b/.github/workflows/xcode.yml index e20b49248f..57128400a5 100644 --- a/.github/workflows/xcode.yml +++ b/.github/workflows/xcode.yml @@ -62,6 +62,7 @@ jobs: test: name: Run Tests runs-on: xcode-27 + timeout-minutes: 30 needs: [build] if: github.event.pull_request.draft == false @@ -103,6 +104,7 @@ jobs: run: | mkdir server/data ./server/occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./server/occ config:system:set force_language --value=en ./server/occ config:system:set hashing_default_password --value=true --type=boolean ./server/occ config:system:set auth.bruteforce.protection.enabled --value false --type bool ./server/occ config:system:set ratelimit.protection.enabled --value false --type bool @@ -116,12 +118,17 @@ jobs: name: Nextcloud for iOS - name: Assert Nextcloud Server Status - run: curl -s --retry 5 --retry-delay 60 --retry-all-errors http://localhost:8080/status.php || true + run: curl --fail --show-error --silent --max-time 5 --retry 5 --retry-delay 2 --retry-max-time 30 --retry-all-errors http://localhost:8080/status.php - name: Boot iOS Simulator run: | xcrun simctl boot "iPhone 17" - xcrun simctl bootstatus "iPhone 17" || echo "Simulator booted" + xcrun simctl bootstatus "iPhone 17" -b + xcrun simctl spawn "iPhone 17" defaults write NSGlobalDomain AppleLanguages -array en + xcrun simctl spawn "iPhone 17" defaults write NSGlobalDomain AppleLocale -string en_US + xcrun simctl shutdown "iPhone 17" + xcrun simctl boot "iPhone 17" + xcrun simctl bootstatus "iPhone 17" -b - name: Check if Safari is installed run: | @@ -133,19 +140,24 @@ jobs: fi - name: Run Xcode Test + timeout-minutes: 25 run: | set -o pipefail && \ xcodebuild test-without-building \ -xctestrun $(find . -type f -name "*.xctestrun") \ -destination "${{ env.DESTINATION }}" \ -derivedDataPath "DerivedData" \ + -skip-testing:NextcloudUITests/PasscodeUITests \ -test-iterations 3 \ -retry-tests-on-failure \ + -test-timeouts-enabled YES \ + -default-test-execution-time-allowance 360 \ + -maximum-test-execution-time-allowance 360 \ -resultBundlePath "TestResult.xcresult" - name: Upload Xcode Test Results uses: actions/upload-artifact@v7 - if: ${{ !cancelled() }} + if: ${{ always() }} with: name: TestResult.xcresult path: "TestResult.xcresult" diff --git a/AGENTS.md b/AGENTS.md index d842b1a70b..18c62c6bca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ Other platforms like Android are irrelevant for this project. |-----------------|-----------------------------------------------------|----------------------| | `.github` | GitHub CI workflows (build & test, SwiftLint, extension-target builds) and issue templates. | Keep all three checks green: `xcode.yml` (builds the `Nextcloud` scheme and runs its tests against a Nextcloud server provisioned in CI), `lint.yml` (SwiftLint), `additional-targets.yml` (build-checks the extension schemes). CI skips draft PRs. | | `iOSClient` | Main iOS client source code. | — | +| `iOSClient/Utility/Passcode` | Shared SwiftUI passcode UI and authentication model for the app and Share Extension; the mandatory app lock uses a dedicated window per scene. | Preserve existing credentials, lockout rules and app/extension reset behavior. | | `iOSClient/Supporting Files` | Translation files synced with Transifex (see `iOSClient/.tx/config`). | Only add new strings to `en.lproj` (`Localizable.strings`, plurals in `Localizable.stringsdict`); never edit the other `*.lproj` dirs — Transifex overwrites them. | | `Brand` | Branding layer: `NCBrand.swift` (app constants, colors), `Database.swift` (Realm database name and schema version), Info.plists and entitlements for all targets, launch screen and intro screens. | Bump `databaseSchemaVersion` in `Brand/Database.swift` when changing Realm models. | | `File Provider Extension` | Contains business logic for the iOS File Provider extension. | — | diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 31a3849673..c9d5cca854 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A0C0DE0230E0000100C0D001 /* NCPasscodeWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */; }; 22BA14432CF44BDBA2E6C291 /* NCDocumentEditorSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B74B947E07444CC9CFE0A6C /* NCDocumentEditorSupportTests.swift */; }; 2C1D5D7623E2DE3300334ABB /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; 2C1D5D7923E2DE9100334ABB /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; @@ -14,6 +15,10 @@ 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 370D26AF248A3D7A00121797 /* NCCellMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370D26AE248A3D7A00121797 /* NCCellMain.swift */; }; 63563FE53573412998852DD3 /* NCAlbumsPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439616AAA50F4360A761E5EF /* NCAlbumsPersistenceTests.swift */; }; + 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; + 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */; }; + 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */; }; A12720261001000000000001 /* NCBackgroundUploadExtensionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F793E7C230417C7100F011FE /* NCBackgroundUploadExtensionManager.swift */; }; A1E2EE010000000000000002 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; A1E2EE010000000000000003 /* NCEndToEndKeySet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E2EE010000000000000001 /* NCEndToEndKeySet.swift */; }; @@ -118,6 +123,7 @@ C0DECA022F65000100C0D001 /* NCCameraRollTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECA012F65000100C0D001 /* NCCameraRollTests.swift */; }; C0DEDA7E30C1000100A00002 /* NCAutoUploadDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEDA7E30C1000100A00001 /* NCAutoUploadDateTests.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; + D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F0A1B2C430B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C330B5000100D4E5F6 /* NCMediaViewerFloatingTitleViewTests.swift */; }; F0A1B2C630B6000100D4E5F6 /* NCImageZoomViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A1B2C530B6000100D4E5F6 /* NCImageZoomViewTests.swift */; }; @@ -174,7 +180,6 @@ F37208C22BAB63F0006B5430 /* TagListView in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C12BAB63F0006B5430 /* TagListView */; }; F37208C42BAB63F0006B5430 /* Gzip in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C32BAB63F0006B5430 /* Gzip */; }; F37208C82BAB63F1006B5430 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = F37208C72BAB63F1006B5430 /* KeychainAccess */; }; - F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */; }; F389C9F52CEE383300049762 /* SelectAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F389C9F42CEE383300049762 /* SelectAlbumView.swift */; }; F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */; }; F39170AD2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */; }; @@ -365,8 +370,6 @@ F7346E2328B0FEBA006CE2D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */; }; F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */ = {isa = PBXBuildFile; productRef = F734B06528E75C0100E180D5 /* TLPhotoPicker */; }; F7362A1F220C853A005101B5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */; }; - F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; - F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */; }; F7381EE1218218C9000B1560 /* NCOffline.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7381EDA218218C9000B1560 /* NCOffline.swift */; }; F7381EE5218218C9000B1560 /* NCOffline.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7381EDE218218C9000B1560 /* NCOffline.storyboard */; }; F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */; }; @@ -982,40 +985,6 @@ F7D496FD2EBFA6D9004F9823 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; }; F7D497082EBFAFD6004F9823 /* NCImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B649B2ADFFAED00014640 /* NCImageCache.swift */; }; F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */; }; - F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; - F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; - F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; - F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; - F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; - F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; - F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; - F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; - F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; - F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; - F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; - F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; - F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; - F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; - F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; - F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; - F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; - F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */; }; - F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */; }; - F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */; }; - F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */; }; - F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */; }; - F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */; }; - F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */; }; - F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */; }; - F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */; }; - F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */; }; - F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */; }; - F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */; }; - F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */; }; - F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */; }; - F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */; }; - F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */; }; - F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */; }; F7D4BF542CA2ED9D00A5E746 /* VLCKitSPM in Frameworks */ = {isa = PBXBuildFile; productRef = F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */; }; F7D56B1A2972405500FA46C4 /* Mantis in Frameworks */ = {isa = PBXBuildFile; productRef = F7D56B192972405500FA46C4 /* Mantis */; }; F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7D60CAE2C941ACB008FBFDD /* NCMediaPinchGesture.swift */; }; @@ -1331,6 +1300,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeWindow.swift; sourceTree = ""; }; + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeModel.swift; sourceTree = ""; }; 2C33C47F23E2C475005F963B /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 2C33C48123E2C475005F963B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; 2C33C48A23E2CC26005F963B /* Notification_Service_Extension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Notification_Service_Extension-Bridging-Header.h"; sourceTree = ""; }; @@ -1474,6 +1445,7 @@ AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; B601AA002F10000100C0D3E5 /* NCLogFilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCLogFilesView.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.swift; sourceTree = ""; }; + BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeUITests.swift; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C0DEA070306D000100000004 /* NCLocalDatabase+AutoUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCLocalDatabase+AutoUpload.swift"; sourceTree = ""; }; @@ -1506,7 +1478,6 @@ F36C514D2E89393C0097E5F7 /* UIView+BlurVibrancy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+BlurVibrancy.swift"; sourceTree = ""; }; F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+SelectTabBarDelegate.swift"; sourceTree = ""; }; F37208742BAB4AB0006B5430 /* TestConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = ""; }; - F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPasscodeView.swift; sourceTree = ""; }; F389C9F42CEE383300049762 /* SelectAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectAlbumView.swift; sourceTree = ""; }; F38F71242B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonSelectTabBar.swift; sourceTree = ""; }; F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileAutoRenamer+Extensions.swift"; sourceTree = ""; }; @@ -1625,7 +1596,6 @@ F7346E2028B0FA3A006CE2D2 /* Widget-Brinding-header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Widget-Brinding-header.h"; sourceTree = ""; }; F7346E2228B0FEBA006CE2D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F7362A1E220C853A005101B5 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscode.swift; sourceTree = ""; }; F7381EDA218218C9000B1560 /* NCOffline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCOffline.swift; sourceTree = ""; }; F7381EDE218218C9000B1560 /* NCOffline.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCOffline.storyboard; sourceTree = ""; }; F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppVersionManager.swift; sourceTree = ""; }; @@ -2075,41 +2045,6 @@ F7CF06822E11273F0063AD04 /* NCManageDatabase+CreateMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+CreateMetadata.swift"; sourceTree = ""; }; F7D1611F23CF19E30039EBBF /* NCViewerRichWorkspace.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerRichWorkspace.storyboard; sourceTree = ""; }; F7D4BF002CA1831600A5E746 /* NCCollectionViewCommonPinchGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommonPinchGesture.swift; sourceTree = ""; }; - F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleImage.h; sourceTree = ""; }; - F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleImage.m; sourceTree = ""; }; - F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewContentLayout.h; sourceTree = ""; }; - F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewContentLayout.m; sourceTree = ""; }; - F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerAnimatedTransitioning.h; sourceTree = ""; }; - F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewControllerAnimatedTransitioning.m; sourceTree = ""; }; - F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOSettingsKeypadImage.h; sourceTree = ""; }; - F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOSettingsKeypadImage.m; sourceTree = ""; }; - F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewControllerConstants.h; sourceTree = ""; }; - F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleButton.h; sourceTree = ""; }; - F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleButton.m; sourceTree = ""; }; - F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeKeypadView.h; sourceTree = ""; }; - F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeKeypadView.m; sourceTree = ""; }; - F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeView.h; sourceTree = ""; }; - F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeView.m; sourceTree = ""; }; - F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadButton.h; sourceTree = ""; }; - F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadButton.m; sourceTree = ""; }; - F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsKeypadView.h; sourceTree = ""; }; - F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsKeypadView.m; sourceTree = ""; }; - F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsWarningLabel.h; sourceTree = ""; }; - F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsWarningLabel.m; sourceTree = ""; }; - F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeButtonLabel.h; sourceTree = ""; }; - F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeButtonLabel.m; sourceTree = ""; }; - F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeCircleView.h; sourceTree = ""; }; - F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeCircleView.m; sourceTree = ""; }; - F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeFixedInputView.h; sourceTree = ""; }; - F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeFixedInputView.m; sourceTree = ""; }; - F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeInputField.h; sourceTree = ""; }; - F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeInputField.m; sourceTree = ""; }; - F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeVariableInputView.h; sourceTree = ""; }; - F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeVariableInputView.m; sourceTree = ""; }; - F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeSettingsViewController.h; sourceTree = ""; }; - F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeSettingsViewController.m; sourceTree = ""; }; - F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPasscodeViewController.h; sourceTree = ""; }; - F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPasscodeViewController.m; sourceTree = ""; }; F7D532461F5D4123006568B1 /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Localizable.strings; sourceTree = ""; }; F7D5324D1F5D4137006568B1 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; F7D532541F5D4155006568B1 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; @@ -2161,6 +2096,7 @@ F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; + F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPasscodeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2439,10 +2375,21 @@ path = Advanced; sourceTree = ""; }; + B6DE60601BB3410D92EB99EC /* Passcode */ = { + isa = PBXGroup; + children = ( + 131F8E2F43AD414E9DB53019 /* NCPasscodeModel.swift */, + A0C0DE0130E0000100C0D001 /* NCPasscodeWindow.swift */, + F902B24E44354B6D9F329F12 /* NCPasscodeView.swift */, + ); + path = Passcode; + sourceTree = ""; + }; C0046CDB2A17B98400D87C9D /* NextcloudUITests */ = { isa = PBXGroup; children = ( F3374A952D6744A4002A38F9 /* BaseUIXCTestCase.swift */, + BC6C501EF1104A6A8569D9C1 /* PasscodeUITests.swift */, AABD0C882D5F631600F009E6 /* Extensions */, AA3C85E92D36BBDE00F74F12 /* UITestBackend */, AA8E03DB2D2FBAAD00E7E89C /* DownloadLimitUITests.swift */, @@ -2581,7 +2528,6 @@ F7A03E342D427308007AA677 /* NCMainNavigationController.swift */, F794E13C2BBBFF2E003693D7 /* NCMainTabBarController.swift */, F75A64A3307DFB9E0059B78E /* NCMainTabBarController+Sidebar.swift */, - F737DA9C2B7B893C0063BAFC /* NCPasscode.swift */, F77444F7222816D5000D5EB0 /* NCPickerViewController.swift */, F75A64A1307DFB870059B78E /* NCSidebarView.swift */, ); @@ -3228,7 +3174,6 @@ children = ( F76882112C0DD1E7001CF441 /* NCSettingsModel.swift */, F768820C2C0DD1E7001CF441 /* NCSettingsView.swift */, - F3754A7C2CF87D600009312E /* SetupPasscodeView.swift */, ); path = Settings; sourceTree = ""; @@ -3617,6 +3562,7 @@ F7BFFA991A24D7BB0044ED85 /* Utility */ = { isa = PBXGroup; children = ( + B6DE60601BB3410D92EB99EC /* Passcode */, F39170A82CB8201B006127BC /* FileAutoRenamer+Extensions.swift */, F33918C32C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift */, F752BA042E58C05200616A26 /* Maintenance.swift */, @@ -3640,7 +3586,6 @@ F3E173BE2C9B1057006D177A /* ScreenAwakeManager */, F71F6D062B6A6A5E00F1EB15 /* ThreadSafeArray.swift */, F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */, - F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */, ); path = Utility; sourceTree = ""; @@ -3730,96 +3675,6 @@ path = RichWorkspace; sourceTree = ""; }; - F7D4BF0A2CA2E8D800A5E746 /* Models */ = { - isa = PBXGroup; - children = ( - F7D4BF022CA2E8D800A5E746 /* TOPasscodeCircleImage.h */, - F7D4BF032CA2E8D800A5E746 /* TOPasscodeCircleImage.m */, - F7D4BF042CA2E8D800A5E746 /* TOPasscodeViewContentLayout.h */, - F7D4BF052CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m */, - F7D4BF062CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.h */, - F7D4BF072CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m */, - F7D4BF082CA2E8D800A5E746 /* TOSettingsKeypadImage.h */, - F7D4BF092CA2E8D800A5E746 /* TOSettingsKeypadImage.m */, - ); - path = Models; - sourceTree = ""; - }; - F7D4BF0C2CA2E8D800A5E746 /* Supporting */ = { - isa = PBXGroup; - children = ( - F7D4BF0B2CA2E8D800A5E746 /* TOPasscodeViewControllerConstants.h */, - ); - path = Supporting; - sourceTree = ""; - }; - F7D4BF132CA2E8D800A5E746 /* Main */ = { - isa = PBXGroup; - children = ( - F7D4BF0D2CA2E8D800A5E746 /* TOPasscodeCircleButton.h */, - F7D4BF0E2CA2E8D800A5E746 /* TOPasscodeCircleButton.m */, - F7D4BF0F2CA2E8D800A5E746 /* TOPasscodeKeypadView.h */, - F7D4BF102CA2E8D800A5E746 /* TOPasscodeKeypadView.m */, - F7D4BF112CA2E8D800A5E746 /* TOPasscodeView.h */, - F7D4BF122CA2E8D800A5E746 /* TOPasscodeView.m */, - ); - path = Main; - sourceTree = ""; - }; - F7D4BF1A2CA2E8D800A5E746 /* Settings */ = { - isa = PBXGroup; - children = ( - F7D4BF142CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.h */, - F7D4BF152CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m */, - F7D4BF162CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.h */, - F7D4BF172CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m */, - F7D4BF182CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.h */, - F7D4BF192CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m */, - ); - path = Settings; - sourceTree = ""; - }; - F7D4BF252CA2E8D800A5E746 /* Shared */ = { - isa = PBXGroup; - children = ( - F7D4BF1B2CA2E8D800A5E746 /* TOPasscodeButtonLabel.h */, - F7D4BF1C2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m */, - F7D4BF1D2CA2E8D800A5E746 /* TOPasscodeCircleView.h */, - F7D4BF1E2CA2E8D800A5E746 /* TOPasscodeCircleView.m */, - F7D4BF1F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.h */, - F7D4BF202CA2E8D800A5E746 /* TOPasscodeFixedInputView.m */, - F7D4BF212CA2E8D800A5E746 /* TOPasscodeInputField.h */, - F7D4BF222CA2E8D800A5E746 /* TOPasscodeInputField.m */, - F7D4BF232CA2E8D800A5E746 /* TOPasscodeVariableInputView.h */, - F7D4BF242CA2E8D800A5E746 /* TOPasscodeVariableInputView.m */, - ); - path = Shared; - sourceTree = ""; - }; - F7D4BF262CA2E8D800A5E746 /* Views */ = { - isa = PBXGroup; - children = ( - F7D4BF132CA2E8D800A5E746 /* Main */, - F7D4BF1A2CA2E8D800A5E746 /* Settings */, - F7D4BF252CA2E8D800A5E746 /* Shared */, - ); - path = Views; - sourceTree = ""; - }; - F7D4BF2B2CA2E8D800A5E746 /* TOPasscodeViewController */ = { - isa = PBXGroup; - children = ( - F7D4BF0A2CA2E8D800A5E746 /* Models */, - F7D4BF0C2CA2E8D800A5E746 /* Supporting */, - F7D4BF262CA2E8D800A5E746 /* Views */, - F7D4BF272CA2E8D800A5E746 /* TOPasscodeSettingsViewController.h */, - F7D4BF282CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m */, - F7D4BF292CA2E8D800A5E746 /* TOPasscodeViewController.h */, - F7D4BF2A2CA2E8D800A5E746 /* TOPasscodeViewController.m */, - ); - path = TOPasscodeViewController; - sourceTree = ""; - }; F7DFB7E9219C5A0500680748 /* Create */ = { isa = PBXGroup; children = ( @@ -4886,6 +4741,7 @@ AAA7BC2E2D3E39F1008F1A22 /* CapabilitiesResponse.swift in Sources */, AA3C85F22D394B3A00F74F12 /* DownloadLimitResponse.swift in Sources */, F3374A962D6744A4002A38F9 /* BaseUIXCTestCase.swift in Sources */, + D56DFFE5D5014F66A7C9FED0 /* PasscodeUITests.swift in Sources */, F37208812BAB5979006B5430 /* TestConstants.swift in Sources */, F33D303E2D8B129600531D64 /* AutoUploadUITests.swift in Sources */, ); @@ -4991,32 +4847,15 @@ F711A4DF2AF92CAE00095DD8 /* NCUtility+Date.swift in Sources */, F78295311F962EFA00A572F5 /* NCEndToEndEncryption.m in Sources */, F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */, - F7D4BF2C2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, F7C630802FFE4F8000257EEB /* UIWindowScene+Extension.swift in Sources */, - F7D4BF2D2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, - F7D4BF2E2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, F7D61EA92EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, - F7D4BF2F2CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, - F7D4BF302CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, - F7D4BF312CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, F71916122E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */, F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */, - F7D4BF322CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, F7CAFE192F168F6000DB35A5 /* NCDebouncer.swift in Sources */, - F7D4BF332CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, F77E8C252E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift in Sources */, - F7D4BF342CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, - F7D4BF352CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, - F7D4BF362CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, - F7D4BF372CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, - F7D4BF382CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, - F7D4BF392CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, - F7D4BF3A2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, F79792482F5EECE100FE9544 /* Font+Extension.swift in Sources */, F751760B307F53680057C9B0 /* NCMediaViewerTransitionSource.swift in Sources */, F7D61EBF2EBF1878007F865B /* NCManageDatabase+Tag.swift in Sources */, - F7D4BF3B2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, - F7D4BF3C2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, F74AF3A5247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */, AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */, @@ -5058,7 +4897,8 @@ F757CC8529E7F88B00F31428 /* NCManageDatabase+Groupfolders.swift in Sources */, F768823A2C0DD230001CF441 /* NCPreferences.swift in Sources */, A1E2EE010000000000000004 /* NCEndToEndKeySet.swift in Sources */, - F737DA9E2B7B893C0063BAFC /* NCPasscode.swift in Sources */, + 87768C6AB8E040219DF85898 /* NCPasscodeModel.swift in Sources */, + 9310075861174A62B1C9FED6 /* NCPasscodeView.swift in Sources */, F7C30DF7291BC0D30017149B /* NCNetworkingE2EEUpload.swift in Sources */, F7C687EC2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, F33918C72C7CD8F2002D9AA1 /* FileNameValidator+Extensions.swift in Sources */, @@ -5232,7 +5072,6 @@ F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */, A1E2EE010000000000000007 /* NCEndToEndKeySet.swift in Sources */, F7CAFE1D2F17A35F00DB35A5 /* ProgressQuantizer.swift in Sources */, - F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */, F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */, @@ -5330,30 +5169,13 @@ F77A6B30301112C5007103E6 /* NCMediaSectionHeader.swift in Sources */, F794E13D2BBBFF2E003693D7 /* NCMainTabBarController.swift in Sources */, F7CBC1252BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.swift in Sources */, - F7D4BF3D2CA2E8D800A5E746 /* TOPasscodeKeypadView.m in Sources */, - F7D4BF3E2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadView.m in Sources */, F7132C722D085AD200B42D6A /* NCTermOfServiceModel.swift in Sources */, F7132C732D085AD200B42D6A /* NCTermOfServiceView.swift in Sources */, F7A08A7C3017441D00470AD3 /* NCMedia+SelectDelegate.swift in Sources */, - F7D4BF3F2CA2E8D800A5E746 /* TOPasscodeFixedInputView.m in Sources */, - F7D4BF402CA2E8D800A5E746 /* TOPasscodeButtonLabel.m in Sources */, F70898692EDDB51700EF85BD /* NCSelectOpen+SelectDelegate.swift in Sources */, - F7D4BF412CA2E8D800A5E746 /* TOPasscodeViewControllerAnimatedTransitioning.m in Sources */, - F7D4BF422CA2E8D800A5E746 /* TOPasscodeSettingsViewController.m in Sources */, - F7D4BF432CA2E8D800A5E746 /* TOPasscodeCircleImage.m in Sources */, F78026102E9CFA3700B63436 /* NCTransfersView.swift in Sources */, - F7D4BF442CA2E8D800A5E746 /* TOPasscodeView.m in Sources */, - F7D4BF452CA2E8D800A5E746 /* TOPasscodeCircleButton.m in Sources */, - F7D4BF462CA2E8D800A5E746 /* TOPasscodeInputField.m in Sources */, - F7D4BF472CA2E8D800A5E746 /* TOSettingsKeypadImage.m in Sources */, - F7D4BF482CA2E8D800A5E746 /* TOPasscodeSettingsWarningLabel.m in Sources */, F3A0B1F12EC76FE800F10B82 /* NCUserStatusModel.swift in Sources */, F3A0B1F22EC76FE800F10B82 /* NCUserStatusView.swift in Sources */, - F7D4BF492CA2E8D800A5E746 /* TOPasscodeVariableInputView.m in Sources */, - F7D4BF4A2CA2E8D800A5E746 /* TOPasscodeCircleView.m in Sources */, - F7D4BF4B2CA2E8D800A5E746 /* TOPasscodeViewContentLayout.m in Sources */, - F7D4BF4C2CA2E8D800A5E746 /* TOPasscodeSettingsKeypadButton.m in Sources */, - F7D4BF4D2CA2E8D800A5E746 /* TOPasscodeViewController.m in Sources */, AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */, F71CFA672F2A07DB007A3AE9 /* NCMediaNetwork.swift in Sources */, F7DF7B422F1A36C100514020 /* HelperBanner.swift in Sources */, @@ -5572,7 +5394,9 @@ F79792472F5EECE100FE9544 /* Font+Extension.swift in Sources */, F768822A2C0DD1E7001CF441 /* NCSettingsModel.swift in Sources */, F7BCE48E301091EA00876F4D /* NCMediaDatePickerViewController.swift in Sources */, - F737DA9D2B7B893C0063BAFC /* NCPasscode.swift in Sources */, + 9F0E3B428AB44A6480190C2D /* NCPasscodeModel.swift in Sources */, + 7670DABED44E4D56AD99E6DC /* NCPasscodeView.swift in Sources */, + A0C0DE0230E0000100C0D001 /* NCPasscodeWindow.swift in Sources */, F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F7CADEFD2EA159210057849E /* NCMetadataUploadTranfersSuccess.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, diff --git a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme index 2758ae60e2..2d901b5ea6 100755 --- a/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme +++ b/Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme @@ -65,6 +65,8 @@ Bool { - guard !NCPasscode.shared.isPasscodeReset else { + guard !NCPasscodeModel.isPasscodeReset else { return showNoAccountAlert() } @@ -575,18 +570,21 @@ extension NCShareExtension { } } -extension NCShareExtension: NCPasscodeDelegate { - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - passcodeViewController.dismiss(animated: false) - self.cancel(with: .noAccount) - } - } - - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) { - if !isCorrectCode { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - passcodeViewController.dismiss(animated: false) +extension NCShareExtension { + /// Require authentication once per foreground session without reopening after dismissal. + private func requestPasscodeIfNeeded() { + guard NCPreferences().presentPasscode, + !isPasscodeAuthenticated, + !isPresentingPasscode else { return } + isPresentingPasscode = true + NCPasscodeView.present(from: self) { [weak self] result in + guard let self else { return } + self.isPresentingPasscode = false + switch result { + case .authenticated: + self.isPasscodeAuthenticated = true + _ = self.validateAccount() + case .cancelled, .reset, .configured, .disabled: self.cancel(with: .noAccount) } } diff --git a/Share/Share-Bridging-Header.h b/Share/Share-Bridging-Header.h index dd577bf8ef..68997c812e 100644 --- a/Share/Share-Bridging-Header.h +++ b/Share/Share-Bridging-Header.h @@ -6,4 +6,3 @@ #import "NCEndToEndEncryption.h" #import "UIImage+animatedGIF.h" -#import "TOPasscodeViewController.h" diff --git a/Tests/NextcloudUITests/BaseUIXCTestCase.swift b/Tests/NextcloudUITests/BaseUIXCTestCase.swift index 5dd1a21de8..734d3f0b82 100644 --- a/Tests/NextcloudUITests/BaseUIXCTestCase.swift +++ b/Tests/NextcloudUITests/BaseUIXCTestCase.swift @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: Nextcloud GmbH // SPDX-FileCopyrightText: 2025 Milen Pivchev +// SPDX-FileCopyrightText: 2026 Marino Faggiana // SPDX-License-Identifier: GPL-3.0-or-later import Foundation @@ -58,92 +59,136 @@ class BaseUIXCTestCase: XCTestCase { try await Task.sleep(for: .seconds(TestConstants.controlExistenceTimeout)) } - /// - /// Automation of the sign-in, if required. - /// - /// + /// Sign in through the app's authentication flow, supporting system authentication and the embedded fallback. func logIn() async throws { - guard app.buttons["login"].exists else { - return + let login = app.buttons["login"] + let deadline = Date().addingTimeInterval(30) + // Startup can restore the main interface after the first accessibility snapshot. + // Wait for either destination instead of assuming that the login screen will appear. + repeat { + if app.buttons["openSidebar"].exists || app.buttons["accountSwitcher"].exists { + return + } + if login.exists && login.isHittable { + break + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline + guard login.exists else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(login) } + login.tap() - app.buttons["login"].tap() - - let serverAddressTextField = app.textFields["serverAddress"].firstMatch - guard serverAddressTextField.await() else { return } - - try await aSmallMoment() - - serverAddressTextField.tap() - serverAddressTextField.typeText(TestConstants.server) - + let serverAddress = app.textFields["serverAddress"].firstMatch + guard serverAddress.await() else { throw UITestError.waitForExistence(serverAddress) } + serverAddress.tap() + serverAddress.typeText(TestConstants.server) app.buttons["submitServerAddress"].tap() - try await aSmallMoment() - - let webView = app.webViews.firstMatch - - guard webView.await() else { - throw UITestError.waitForExistence(webView) + let webView = try await authenticationWebView() + let username = webView.textFields.firstMatch + let loginButton = webView.buttons["Log in"].firstMatch + if !username.exists, loginButton.await(timeout: 5) { + loginButton.tap() } - -// try await aSmallMoment() - - let loginButton = webView.buttons["Log in"] - -// try await aSmallMoment() - - if loginButton.await() { + if username.await() { + username.tap() + username.typeText(TestConstants.username) + let password = webView.secureTextFields.firstMatch + guard password.await() else { throw UITestError.waitForExistence(password) } + password.tap() + password.typeText(TestConstants.password) + guard loginButton.await() else { throw UITestError.waitForExistence(loginButton) } loginButton.tap() } -// try await aSmallMoment() - - let usernameTextField = webView.textFields.firstMatch - - if usernameTextField.await() { - - try await aSmallMoment() - - guard usernameTextField.await() else { return } - usernameTextField.tap() - - try await aSmallMoment() - - usernameTextField.typeText(TestConstants.username) - - try await aSmallMoment() - - let passwordSecureTextField = webView.secureTextFields.firstMatch - - try await aSmallMoment() - - passwordSecureTextField.tap() - - - try await aSmallMoment() - - passwordSecureTextField.typeText(TestConstants.password) - - try await aSmallMoment() - - webView.buttons.firstMatch.tap() + let grant = webView.buttons["Grant access"] + guard try await waitForAuthenticationConsent() else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(grant) } - try await aSmallMoment() - - let grantButton = webView.buttons["Grant access"] - - guard grantButton.await() else { - throw UITestError.waitForExistence(grantButton) + let sidebar = app.buttons["openSidebar"].firstMatch + guard sidebar.await(timeout: 30) || app.buttons["accountSwitcher"].exists else { + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(sidebar) } + } + + /// Waiting alone does not trigger interruption monitors; dismiss password prompts in their owning process. + private func waitForAuthenticationConsent() async throws -> Bool { + let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] + let hosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] + let deadline = Date().addingTimeInterval(30) + var passwordPromptDismissed = false + repeat { + promptSearch: for host in hosts where !passwordPromptDismissed && host.state != .notRunning { + for label in ["Not Now", "Non ora", "Ora no"] { + let button = host.buttons[label].firstMatch + if button.exists && button.isHittable { + button.tap() + passwordPromptDismissed = true + _ = button.waitForNonExistence(timeout: 5) + break promptSearch + } + } + } + for browser in browsers where browser.state != .notRunning { + let grant = browser.webViews.buttons["Grant access"].firstMatch + if grant.exists && grant.isHittable { + grant.tap() + return true + } + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline + return false + } - grantButton.tap() - grantButton.awaitInexistence() + /// ASWebAuthenticationSession may expose its UI in SafariViewService rather than in the app hierarchy. + private func authenticationWebView() async throws -> XCUIElement { + let browsers = [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService")] + let promptHosts = browsers + [XCUIApplication(bundleIdentifier: "com.apple.springboard")] + // The app tries HTTPS first, even for the HTTP-only localhost test server. + // Allow the initial URLSession request to time out before its HTTP fallback starts. + let timeout = URLSessionConfiguration.default.timeoutIntervalForRequest + 30 + let deadline = Date().addingTimeInterval(timeout) + repeat { + for host in promptHosts where host.state != .notRunning { + for label in ["Continue", "Allow"] { + let button = host.alerts.buttons[label].firstMatch + if button.exists && button.isHittable { + button.tap() + } + } + } + for browser in browsers where browser.state != .notRunning { + let webView = browser.webViews.firstMatch + if webView.exists { + return webView + } + } + try await Task.sleep(for: .milliseconds(250)) + } while Date() < deadline - app.buttons["accountSwitcher"].await() + attachAuthenticationDiagnostics() + throw UITestError.waitForExistence(app.webViews.firstMatch) + } - try await aSmallMoment() + /// Keep login diagnostics in the xcresult instead of losing the context on a CI failure. + private func attachAuthenticationDiagnostics() { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "Authentication screen" + screenshot.lifetime = .keepAlways + add(screenshot) + for host in [app!, XCUIApplication(bundleIdentifier: "com.apple.SafariViewService"), XCUIApplication(bundleIdentifier: "com.apple.springboard")] + where host.state == .runningForeground { + let hierarchy = XCTAttachment(string: host.debugDescription) + hierarchy.name = "Authentication UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + } } /// diff --git a/Tests/NextcloudUITests/PasscodeUITests.swift b/Tests/NextcloudUITests/PasscodeUITests.swift new file mode 100644 index 0000000000..509fa9f371 --- /dev/null +++ b/Tests/NextcloudUITests/PasscodeUITests.swift @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import XCTest + +/// Exercises real settings and app-lock flows against the standard UI-test server. +@MainActor +final class PasscodeUITests: BaseUIXCTestCase { + private let originalCode = "135790" + private let replacementCode = "246801" + private let wrongCode = "000000" + private var currentCode: String? + private let languageArguments = ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] + + private var title: XCUIElement { app.staticTexts["passcode.title"] } + private var closeButton: XCUIElement { app.buttons["passcode.close"] } + private var lockWindow: XCUIElement { app.windows["passcode.window"] } + private var lockout: XCUIElement { app.staticTexts["passcode.lockout.remaining"] } + private var lockSetting: XCUIElement { app.buttons["settings.passcode.toggle"] } + private var changeSetting: XCUIElement { app.buttons["settings.passcode.change"] } + + override func setUp() async throws { + try await super.setUp() + continueAfterFailure = false + addUIInterruptionMonitor(withDescription: "Allow Notifications", for: "Allow") + app = XCUIApplication() + app.launchArguments = ["UI_TESTING", "UI_TESTING_PASSCODE"] + languageArguments + app.launch() + try await logIn() + openSettings() + XCTAssertFalse(changeSetting.exists, "Each test must start with app lock disabled.") + } + + override func tearDown() async throws { + // Avoid leaving the simulator locked if an assertion interrupted a test. + if let currentCode, app != nil { + app.activate() + if lockout.exists { + _ = lockout.waitForNonExistence(timeout: 65) + } + if title.exists && !closeButton.exists { + clearPartialCode() + enter(currentCode) + } + if closeButton.exists { + closeButton.tap() + } + if !lockSetting.exists { + openSettings() + } + if changeSetting.exists { + lockSetting.tap() + enter(currentCode) + _ = title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout) + } + } + app?.terminate() + try await super.tearDown() + } + + func testCreateChangeAndDisablePasscode() { + createPasscode() + changeSetting.tap() + assertTitle("Enter Passcode") + enter(originalCode) + assertTitle("Enter a new passcode") + enter(replacementCode) + assertTitle("Confirm new passcode") + enter(replacementCode) + currentCode = replacementCode + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + + lockSetting.tap() + assertTitle("Enter Passcode") + enter(originalCode) + XCTAssertTrue(app.staticTexts["passcode.error"].waitForExistence(timeout: 5)) + XCTAssertEqual(app.staticTexts["passcode.error"].label, "Incorrect passcode. Try again.") + enter(replacementCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertFalse(changeSetting.exists) + currentCode = nil + } + + /// Regression for the reported brute-force bypass when disabling app lock. + func testDisableLockoutSurvivesReopeningBackgroundAndRelaunch() { + createPasscode() + lockSetting.tap() + enter(wrongCode) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + enter(wrongCode) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + enter(wrongCode) + assertLockedOut() + let deadline = Date().addingTimeInterval(TimeInterval(remainingSeconds())) + + // Reopening must neither enable the keypad nor restart the countdown. + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + lockSetting.tap() + assertLockedOut() + waitForCountdown(toDropBelow: 56) + assertOriginalDeadline(deadline) + + // Background cancels the optional request; foreground requires the mandatory lock. + backgroundAndActivate() + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + assertLockedOut() + assertOriginalDeadline(deadline) + + // Relaunch without setup flags: credentials and the deadline must persist in Keychain. + app.terminate() + app.launchArguments = languageArguments + app.launch() + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + assertLockedOut() + assertOriginalDeadline(deadline) + + XCTAssertTrue(lockout.waitForNonExistence(timeout: max(0, deadline.timeIntervalSinceNow) + 5)) + enter(originalCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + } + + /// Changing the code must enforce the same persisted protection as disabling it. + func testChangePasscodeCannotResetFailedAttemptsByReopening() { + createPasscode() + for attempt in 1...3 { + changeSetting.tap() + assertTitle("Enter Passcode") + enter(wrongCode) + if attempt < 3 { + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + } + } + assertLockedOut() + let deadline = Date().addingTimeInterval(TimeInterval(remainingSeconds())) + closeButton.tap() + XCTAssertTrue(title.waitForNonExistence(timeout: 5)) + changeSetting.tap() + assertLockedOut() + assertOriginalDeadline(deadline) + } + + func testBackgroundCannotBypassMandatoryAppLock() { + createPasscode() + backgroundAndActivate() + for _ in 0..<2 { + assertTitle("Enter Passcode") + XCTAssertFalse(closeButton.exists) + XCTAssertFalse(lockSetting.isHittable, "Protected settings must remain inaccessible.") + let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.25)) + let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8)) + start.press(forDuration: 0.1, thenDragTo: end) + XCTAssertTrue(title.exists, "Dragging must not dismiss the mandatory lock.") + backgroundAndActivate() + } + enter(originalCode) + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertTrue(lockSetting.waitForExistence(timeout: 5), "Unlocking must preserve the Settings screen.") + } + + /// An informational sheet stays in the app window while the mandatory lock covers it. + func testAppLockAppearsAbovePresentedSheet() { + createPasscode() + let acknowledgements = app.buttons["Acknowledgements"] + scrollTo(acknowledgements) + acknowledgements.tap() + let close = app.buttons["settings.acknowledgements.close"] + XCTAssertTrue(close.waitForExistence(timeout: 5)) + + backgroundAndActivate() + guard lockWindow.waitForExistence(timeout: 5) else { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "App lock above acknowledgements" + screenshot.lifetime = .keepAlways + add(screenshot) + let hierarchy = XCTAttachment(string: app.debugDescription) + hierarchy.name = "App lock UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + XCTFail("App-lock window missing after foreground. Passcode title exists: \(title.exists); acknowledgements close exists: \(close.exists).") + return + } + assertTitle("Enter Passcode") + XCTAssertFalse(close.isHittable) + enter(originalCode) + XCTAssertTrue(lockWindow.waitForNonExistence(timeout: 5)) + XCTAssertTrue(close.isHittable, "Unlocking must preserve the sheet underneath.") + close.tap() + scrollTo(lockSetting, towardsTop: true) + } + + /// An existing confirmation alert cannot intercept or prevent the app-lock window. + func testAppLockAppearsAbovePresentedAlert() { + createPasscode() + let advanced = app.buttons["Advanced"] + scrollTo(advanced) + advanced.tap() + let clearCache = app.buttons["Clear cache"] + scrollTo(clearCache) + clearCache.tap() + let cancel = app.alerts.firstMatch.buttons["Cancel"] + XCTAssertTrue(cancel.waitForExistence(timeout: 5)) + + backgroundAndActivate() + XCTAssertTrue(lockWindow.waitForExistence(timeout: 5)) + assertTitle("Enter Passcode") + XCTAssertFalse(cancel.isHittable) + enter(originalCode) + XCTAssertTrue(lockWindow.waitForNonExistence(timeout: 5)) + XCTAssertTrue(cancel.isHittable, "Unlocking must preserve the confirmation alert underneath.") + cancel.tap() + app.navigationBars["Advanced"].buttons["Settings"].tap() + scrollTo(lockSetting, towardsTop: true) + } + + /// Waiting out each three-attempt lockout must not reset the ten-attempt app-wipe counter. + func testAppResetAfterTenFailuresAcrossLockouts() { + createPasscode() + let resetSetting = app.switches["settings.passcode.reset"] + for _ in 0..<3 where !resetSetting.isHittable { + app.swipeUp() + } + XCTAssertTrue(resetSetting.waitForExistence(timeout: 5)) + // SwiftUI can expose the labelled row as a switch containing the actual control. + let nestedSwitch = resetSetting.switches.firstMatch + let resetControl = nestedSwitch.exists ? nestedSwitch : resetSetting + XCTAssertEqual(resetControl.value as? String, "0") + if nestedSwitch.exists { + resetControl.tap() + } else { + // In the English test layout the switch is at the trailing edge of the row. + resetControl.coordinate(withNormalizedOffset: CGVector(dx: 0.95, dy: 0.5)).tap() + } + let resetEnabled = XCTNSPredicateExpectation(predicate: NSPredicate(format: "value == %@", "1"), object: resetControl) + guard XCTWaiter.wait(for: [resetEnabled], timeout: 5) == .completed else { + let screenshot = XCTAttachment(screenshot: XCUIScreen.main.screenshot()) + screenshot.name = "Reset setting after tap" + screenshot.lifetime = .keepAlways + add(screenshot) + let hierarchy = XCTAttachment(string: app.debugDescription) + hierarchy.name = "Reset setting UI hierarchy" + hierarchy.lifetime = .keepAlways + add(hierarchy) + XCTFail("The reset switch did not become enabled after tapping it.") + return + } + + backgroundAndActivate() + assertTitle("Enter Passcode") + for attempt in 1...10 { + enter(wrongCode) + if attempt.isMultiple(of: 3) { + assertLockedOut() + XCTAssertTrue(lockout.waitForNonExistence(timeout: 65)) + XCTAssertTrue(app.buttons["passcode.digit.1"].isEnabled) + } else if attempt < 10 { + XCTAssertTrue(title.exists, "The app must remain locked until the reset threshold.") + } + } + + // The real reset clears local data and exits. Relaunch without the test-setup reset. + XCTAssertTrue(app.wait(for: .notRunning, timeout: 10)) + currentCode = nil + app.launchArguments = languageArguments + app.launch() + XCTAssertTrue(app.buttons["login"].waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertFalse(title.exists, "The app reset must remove the stored passcode.") + } + + private func createPasscode() { + lockSetting.tap() + assertTitle("Enter a new passcode") + enter(originalCode) + assertTitle("Confirm new passcode") + enter(originalCode) + currentCode = originalCode + XCTAssertTrue(title.waitForNonExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertTrue(changeSetting.waitForExistence(timeout: 5)) + } + + private func openSettings() { + let sidebar = app.buttons["openSidebar"].firstMatch + XCTAssertTrue(sidebar.waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + sidebar.tap() + let settings = app.buttons["sidebarSettings"] + XCTAssertTrue(settings.waitForExistence(timeout: 5)) + settings.tap() + XCTAssertTrue(lockSetting.waitForExistence(timeout: 5)) + } + + private func scrollTo(_ element: XCUIElement, towardsTop: Bool = false) { + for _ in 0..<6 where !element.isHittable { + if towardsTop { + app.swipeDown() + } else { + app.swipeUp() + } + } + XCTAssertTrue(element.isHittable) + } + + private func enter(_ code: String) { + for digit in code { + let button = app.buttons["passcode.digit.\(digit)"] + XCTAssertTrue(button.waitForExistence(timeout: 5)) + XCTAssertTrue(button.isEnabled) + button.tap() + } + } + + private func clearPartialCode() { + let delete = app.buttons["passcode.delete"] + for _ in 0..<6 where delete.exists && delete.isEnabled { + delete.tap() + } + } + + private func assertTitle(_ expected: String) { + XCTAssertTrue(title.waitForExistence(timeout: TestConstants.controlExistenceTimeout)) + XCTAssertEqual(title.label, expected) + } + + /// Wait for the Home screen and an actual background state before reactivating the app. + private func backgroundAndActivate() { + XCUIDevice.shared.press(.home) + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + XCTAssertTrue(springboard.wait(for: .runningForeground, timeout: 5)) + let background = NSPredicate { [app] _, _ in + MainActor.assumeIsolated { + app?.state == .runningBackground || app?.state == .runningBackgroundSuspended + } + } + XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: background, object: nil)], timeout: 5), .completed) + app.activate() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5)) + } + + private func assertLockedOut() { + XCTAssertTrue(lockout.waitForExistence(timeout: 5)) + XCTAssertFalse(app.buttons["passcode.digit.1"].isEnabled) + } + + private func remainingSeconds() -> Int { + guard let first = lockout.label.split(separator: " ").first, let seconds = Int(first) else { + XCTFail("Missing lockout countdown: \(lockout.label)") + return 0 + } + return seconds + } + + private func waitForCountdown(toDropBelow seconds: Int) { + let countdown = NSPredicate { [weak self] _, _ in + MainActor.assumeIsolated { + guard let self else { return false } + return self.remainingSeconds() < seconds + } + } + XCTAssertEqual(XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: countdown, object: nil)], timeout: 10), .completed) + } + + private func assertOriginalDeadline(_ deadline: Date) { + let expected = Int(deadline.timeIntervalSinceNow.rounded(.up)) + XCTAssertGreaterThan(remainingSeconds(), 0) + XCTAssertLessThanOrEqual(remainingSeconds(), expected + 2, "Background, reopening or relaunch must not restart the lockout.") + XCTAssertGreaterThanOrEqual(remainingSeconds(), max(0, expected - 2), "The persisted deadline must not be shortened.") + } +} diff --git a/Tests/Server.sh b/Tests/Server.sh index bf71b8c88a..04c16786ff 100644 --- a/Tests/Server.sh +++ b/Tests/Server.sh @@ -25,6 +25,9 @@ docker run \ echo "Please wait until the server is provisioned…" sleep 20 +# Keep web authentication labels consistent with the UI tests. +docker exec --user www-data $CONTAINER_NAME php /var/www/html/occ config:system:set force_language --value=en + # Enable File Download Limit App. docker exec $CONTAINER_NAME su www-data -c "git clone --depth 1 -b $SERVER_VERSION https://github.com/nextcloud/files_downloadlimit.git /var/www/html/apps/files_downloadlimit/" docker exec $CONTAINER_NAME su www-data -c "php /var/www/html/occ app:enable files_downloadlimit" diff --git a/iOSClient/AppDelegate.swift b/iOSClient/AppDelegate.swift index 1f755c14dc..56a4b60930 100644 --- a/iOSClient/AppDelegate.swift +++ b/iOSClient/AppDelegate.swift @@ -32,11 +32,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var pushSubscriptionTask: Task? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - if isUiTestingEnabled { - Task { - await NCAccount().deleteAllAccounts() - } +#if DEBUG + // Give security UI tests a repeatable starting state on their dedicated simulator. + if isUiTestingEnabled && ProcessInfo.processInfo.arguments.contains("UI_TESTING_PASSCODE") { + let preferences = NCPreferences() + preferences.passcode = nil + preferences.clearPasscodeFailures() + preferences.touchFaceID = false + preferences.requestPasscodeAtStart = true + preferences.resetAppCounterFail = false } +#endif let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 6b8a628966..500f3973eb 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -331,47 +331,6 @@ extension UIAlertController { } } - @MainActor - static func failedPasscode(presenter: UIViewController, completion: (() -> Void)? = nil) { - let preferences = NCPreferences() - let deadline: Date - - if let pending = preferences.passcodeLockoutEnd { - guard pending > Date() else { - endPasscodeLockout(completion: completion) - return - } - - deadline = pending - } else { - deadline = Date().addingTimeInterval(TimeInterval(NCBrandOptions.shared.passcodeSecondsFail)) - preferences.passcodeLockoutEnd = deadline - } - - let alertController = UIAlertController(title: NSLocalizedString("_passcode_counter_fail_", comment: ""), message: nil, preferredStyle: .alert) - presenter.present(alertController, animated: true) - - alertController.message = "\(Int(deadline.timeIntervalSinceNow.rounded(.up))) " + NSLocalizedString("_seconds_", comment: "") - - _ = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in - let seconds = Int(deadline.timeIntervalSinceNow.rounded(.up)) - - if seconds > 0 { - alertController.message = "\(seconds) " + NSLocalizedString("_seconds_", comment: "") - } else { - timer.invalidate() - alertController.dismiss(animated: true) - endPasscodeLockout(completion: completion) - } - } - } - - private static func endPasscodeLockout(completion: (() -> Void)?) { - NCPreferences().clearPasscodeFailures() - - completion?() - } - /// Presents a localized confirmation alert and asynchronously returns the user's choice. /// /// - Parameters: diff --git a/iOSClient/Main/NCMainTabBarController.swift b/iOSClient/Main/NCMainTabBarController.swift index cb2e77b644..6d6e09f981 100644 --- a/iOSClient/Main/NCMainTabBarController.swift +++ b/iOSClient/Main/NCMainTabBarController.swift @@ -100,10 +100,11 @@ class NCMainTabBarController: UITabBarController { previousIndex = selectedIndex if NCBrandOptions.shared.enforce_passcode_lock && NCPreferences().passcode.isEmptyOrNil { - let vc = UIHostingController(rootView: SetupPasscodeView(isLockActive: .constant(false), controller: self)) - vc.isModalInPresentation = true - - present(vc, animated: true) + NCPasscodeView.present(from: self, mode: .create) { result in + if result == .reset { + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + } + } } } diff --git a/iOSClient/Main/NCPasscode.swift b/iOSClient/Main/NCPasscode.swift deleted file mode 100644 index df3bb4e846..0000000000 --- a/iOSClient/Main/NCPasscode.swift +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import UIKit -import LocalAuthentication - -public protocol NCPasscodeDelegate: AnyObject { - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) - func requestedAccount(controller: UIViewController?) -} - -public extension NCPasscodeDelegate { - func evaluatePolicy(_ passcodeViewController: TOPasscodeViewController, isCorrectCode: Bool) {} - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) {} - func requestedAccount(controller: UIViewController?) {} -} - -class NCPasscode: NSObject, TOPasscodeViewControllerDelegate { - public static let shared: NCPasscode = { - let instance = NCPasscode() - return instance - }() - var isPasscodeReset: Bool { - let passcodeCounterFailReset = NCPreferences().passcodeCounterFailReset - return NCPreferences().resetAppCounterFail && passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts - } - - var isPasscodeCounterFail: Bool { - let passcodeCounterFail = NCPreferences().passcodeCounterFail - return passcodeCounterFail >= 3 - } - - var passcodeViewController: TOPasscodeViewController! - var delegate: NCPasscodeDelegate? - var viewController: UIViewController? - - override init() { - super.init() - - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidBecomeActive), - name: UIApplication.didBecomeActiveNotification, - object: nil) - } - - func presentPasscode(viewController: UIViewController, delegate: NCPasscodeDelegate?, completion: @escaping () -> Void) { - if viewController.presentedViewController is TOPasscodeViewController { return } - - var error: NSError? - self.delegate = delegate - self.viewController = viewController - - passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: false) - passcodeViewController.delegate = self - passcodeViewController.keypadButtonShowLettering = false - if NCPreferences().touchFaceID, LAContext().canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - if error == nil { - if LAContext().biometryType == .faceID { - passcodeViewController.biometryType = .faceID - } else if LAContext().biometryType == .touchID { - passcodeViewController.biometryType = .touchID - } - passcodeViewController.allowBiometricValidation = true - passcodeViewController.automaticallyPromptForBiometricValidation = false - } - } - viewController.presentedViewController?.dismiss(animated: false) - viewController.present(passcodeViewController, animated: true, completion: { - // `present` always runs on main thread so this assumption is correct. - MainActor.assumeIsolated { - self.presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: self.passcodeViewController) - } - completion() - }) - } - - func enableTouchFaceID() { - guard NCPreferences().touchFaceID, - NCPreferences().presentPasscode, - !isPasscodeCounterFail, - let passcodeViewController - else { return } - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, evaluateError in - if success { - DispatchQueue.main.async { - passcodeViewController.dismiss(animated: true) { - NCPreferences().passcodeCounterFail = 0 - NCPreferences().passcodeCounterFailReset = 0 - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: true) - if NCPreferences().accountRequest { - self.delegate?.requestedAccount(controller: self.viewController) - } - } - } - } else { - if let error = evaluateError { - switch error._code { - case LAError.userFallback.rawValue, LAError.authenticationFailed.rawValue: - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - // The biometry reply arrives off the main thread. - DispatchQueue.main.async { - self.presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } - case LAError.biometryLockout.rawValue: - LAContext().evaluatePolicy(LAPolicy.deviceOwnerAuthentication, localizedReason: NSLocalizedString("_deviceOwnerAuthentication_", comment: ""), reply: { success, _ in - if success { - DispatchQueue.main.async { - NCPreferences().passcodeCounterFail = 0 - self.enableTouchFaceID() - } - } - }) - default: - break - } - } - } - } - } - } - - func didInputCorrectPasscode(in passcodeViewController: TOPasscodeViewController) { - DispatchQueue.main.async { - passcodeViewController.dismiss(animated: true) { - NCPreferences().passcodeCounterFail = 0 - NCPreferences().passcodeCounterFailReset = 0 - if NCPreferences().accountRequest { - self.delegate?.requestedAccount(controller: self.viewController) - } - } - } - } - - func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode code: String) -> Bool { - if code == NCPreferences().passcode { - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: true) - return true - } else { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - // Keypad taps are delivered on the main thread. - MainActor.assumeIsolated { - presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } - - self.delegate?.evaluatePolicy(passcodeViewController, isCorrectCode: false) - return false - } - } - - func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - enableTouchFaceID() - } - - @MainActor - private func presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: TOPasscodeViewController) { - guard passcodeViewController.presentedViewController == nil else { return } - - // A lockout can elapse while the app is closed; clear it before deciding whether to cover the keypad. - if let lockoutEnd = NCPreferences().passcodeLockoutEnd, lockoutEnd <= Date() { - NCPreferences().clearPasscodeFailures() - } - - if isPasscodeReset { - passcodeViewController.setContentHidden(true, animated: true) - - let alertController = UIAlertController(title: NSLocalizedString("_reset_wrong_passcode_", comment: ""), message: nil, preferredStyle: .alert) - passcodeViewController.present(alertController, animated: true) - - delegate?.passcodeReset(passcodeViewController) - } else if isPasscodeCounterFail { - passcodeViewController.setContentHidden(true, animated: true) - - UIAlertController.failedPasscode(presenter: passcodeViewController) { - passcodeViewController.setContentHidden(false, animated: true) - self.enableTouchFaceID() - } - } - } - - @MainActor - @objc private func applicationDidBecomeActive() { - guard let passcodeViewController, - passcodeViewController.view.window != nil - else { return } - - presentTooManyFailedAttemptsAlertIfNeeded(passcodeViewController: passcodeViewController) - } -} diff --git a/iOSClient/Nextcloud-Bridging-Header.h b/iOSClient/Nextcloud-Bridging-Header.h index 465ea9cd9a..fe04e1a63f 100644 --- a/iOSClient/Nextcloud-Bridging-Header.h +++ b/iOSClient/Nextcloud-Bridging-Header.h @@ -6,4 +6,3 @@ #import "NYMnemonic.h" #import "UIImage+animatedGIF.h" #import "NCPushNotificationEncryption.h" -#import "TOPasscodeViewController.h" diff --git a/iOSClient/SceneDelegate.swift b/iOSClient/SceneDelegate.swift index f6850cb917..c1ef0c01b5 100644 --- a/iOSClient/SceneDelegate.swift +++ b/iOSClient/SceneDelegate.swift @@ -16,6 +16,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private let appDelegate = UIApplication.shared.delegate as? AppDelegate private var privacyProtectionWindow: UIWindow? + private var passcodeWindow: NCPasscodeWindow? private let global = NCGlobal.shared private let alreadyMigratedMultiDomains = UserDefaults.standard.bool(forKey: NCGlobal.shared.udMigrationMultiDomains) @@ -112,6 +113,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // NO account found, start with the Login // NCPreferences().removeAll() + passcodeWindow?.hide() + passcodeWindow = nil if let bundleID = Bundle.main.bundleIdentifier { UserDefaults.standard.removePersistentDomain(forName: bundleID) @@ -184,6 +187,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // window?.rootViewController = controller window?.makeKeyAndVisible() + // Also protect interfaces created after an asynchronous maintenance or migration. + requestPasscodeIfNeeded(controller: controller) // if activateSceneForAccount { self.activateSceneForAccount(scene, account: activeTblAccount.account, controller: controller) @@ -199,6 +204,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneDidDisconnect(_ scene: UIScene) { guard let windowScene = scene as? UIWindowScene else { return } + passcodeWindow?.hide() + passcodeWindow = nil + hidePrivacyProtectionWindow() LucidBannerRegistry.shared.remove(for: windowScene) lucidBanner = nil @@ -208,15 +216,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillEnterForeground(_ scene: UIScene) { hidePrivacyProtectionWindow() - if let rootHostingController = scene.rootHostingController() { - if rootHostingController.anyRootView is Maintenance { - return - } + guard let controller = window?.rootViewController as? NCMainTabBarController else { + return } - let session = SceneManager.shared.getSession(scene: scene) - let controller = SceneManager.shared.getController(scene: scene) - activateSceneForAccount(scene, account: session.account, controller: controller) + activateSceneForAccount(scene, account: controller.account, controller: controller) } func sceneDidBecomeActive(_ scene: UIScene) { @@ -255,16 +259,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let tblAccount = NCManageDatabase.shared.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account)) else { return } + if NCPreferences().presentPasscode { + showPrivacyProtectionWindow() + } bgID = app.beginBackgroundTask(withName: "FlushBeforeSuspend") { app.endBackgroundTask(bgID); bgID = .invalid } Task { - Task { @MainActor in - if NCPreferences().presentPasscode { - showPrivacyProtectionWindow() - } - } defer { app.endBackgroundTask(bgID); bgID = .invalid } @@ -509,14 +511,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func showPrivacyProtectionWindow() { - guard let windowScene = self.window?.windowScene else { + guard privacyProtectionWindow == nil, + let windowScene = self.window?.windowScene else { return } self.privacyProtectionWindow = UIWindow(windowScene: windowScene) self.privacyProtectionWindow?.rootViewController = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController() self.privacyProtectionWindow?.windowLevel = .alert + 1 - self.privacyProtectionWindow?.makeKeyAndVisible() + self.privacyProtectionWindow?.isHidden = false } private func hidePrivacyProtectionWindow() { @@ -524,23 +527,60 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { privacyProtectionWindow = nil } + /// Request the mandatory lock; return whether authentication is required, including while the UI is not ready. + @discardableResult + private func requestPasscodeIfNeeded(controller: NCMainTabBarController) -> Bool { + guard NCPreferences().presentPasscode else { + passcodeWindow?.hide() + passcodeWindow = nil + return false + } + guard !controller.account.isEmpty, + NCManageDatabase.shared.getTableAccount(account: controller.account) != nil, + let window, + window.rootViewController === controller, + let windowScene = window.windowScene else { + return true + } + if passcodeWindow == nil { + passcodeWindow = NCPasscodeWindow(mainWindow: window, windowScene: windowScene) { [weak self, weak controller] result in + guard let self else { return } + self.passcodeWindow = nil + switch result { + case .authenticated: + if NCPreferences().accountRequest, + let controller, + self.window?.rootViewController === controller, + NCManageDatabase.shared.getTableAccount(account: controller.account) != nil { + self.requestedAccount(controller: controller) + } + case .reset: + self.appDelegate?.resetApplication() + case .cancelled, .configured, .disabled: + break + } + } + } + passcodeWindow?.show() + return true + } + private func activateSceneForAccount(_ scene: UIScene, account: String, controller: NCMainTabBarController?) { - guard !account.isEmpty else { + guard !account.isEmpty, + let controller, + controller.account == account, + NCManageDatabase.shared.getTableAccount(account: account) != nil, + let window, + window.rootViewController === controller, + let windowScene = scene as? UIWindowScene, + window.windowScene === windowScene else { return } - if let window = SceneManager.shared.getWindow(scene: scene), - let controller = SceneManager.shared.getController(scene: scene) { - window.rootViewController = controller - if NCPreferences().presentPasscode { - NCPasscode.shared.presentPasscode(viewController: controller, delegate: self) { - NCPasscode.shared.enableTouchFaceID() - } - } else if NCPreferences().accountRequest { - requestedAccount(controller: controller) - } + if !requestPasscodeIfNeeded(controller: controller), NCPreferences().accountRequest { + requestedAccount(controller: controller) } Task { @@ -562,8 +602,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // MARK: - Extension -extension SceneDelegate: NCPasscodeDelegate { +extension SceneDelegate { func requestedAccount(controller: UIViewController?) { + guard var presenter = controller else { return } + while let presented = presenter.presentedViewController { + if let popup = presented as? NCPopupViewController, popup.contentController is NCAccountRequest { + return + } + presenter = presented + } let tblAccounts = NCManageDatabase.shared.getAllTableAccount() if tblAccounts.count > 1, let accountRequestVC = UIStoryboard(name: "NCAccountRequest", bundle: nil).instantiateInitialViewController() as? NCAccountRequest { accountRequestVC.controller = controller @@ -575,7 +622,7 @@ extension SceneDelegate: NCPasscodeDelegate { accountRequestVC.delegate = self accountRequestVC.startTimer(nil) - let availableHeight = controller?.view.safeAreaLayoutGuide.layoutFrame.height ?? window?.safeAreaLayoutGuide.layoutFrame.height ?? 0 + let availableHeight = presenter.view.safeAreaLayoutGuide.layoutFrame.height let maximumHeight = availableHeight * 0.8 let numberCell = tblAccounts.count let height = min(CGFloat(numberCell * Int(accountRequestVC.heightCell) + 45), maximumHeight) @@ -583,13 +630,9 @@ extension SceneDelegate: NCPasscodeDelegate { let popup = NCPopupViewController(contentController: accountRequestVC, popupWidth: 300, popupHeight: height + 20) popup.backgroundAlpha = 0.8 - controller?.present(popup, animated: true) + presenter.present(popup, animated: true) } } - - func passcodeReset(_ passcodeViewController: TOPasscodeViewController) { - appDelegate?.resetApplication() - } } extension SceneDelegate: NCAccountRequestDelegate { diff --git a/iOSClient/Settings/Acknowledgements.rtf b/iOSClient/Settings/Acknowledgements.rtf index 739f9a05e5..9ca49c01cb 100644 --- a/iOSClient/Settings/Acknowledgements.rtf +++ b/iOSClient/Settings/Acknowledgements.rtf @@ -1,247 +1,471 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2868 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fswiss\fcharset0 Helvetica-Bold;} -{\colortbl;\red255\green255\blue255;} -{\*\expandedcolortbl;;} -\paperw12240\paperh15840\margl1440\margr1440\vieww25280\viewh28800\viewkind0 -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 - -\f0\fs24 \cf0 \ -This software contains additional third party software. \ -All the third party software included or linked is redistributed under the terms and conditions of their original licenses. \ +{\rtf1\ansi\ansicpg1252\uc1 +{\fonttbl\f0\fswiss Helvetica;\f1\fswiss Helvetica-Bold;} +\paperw12240\paperh15840\margl1440\margr1440 +\pard\f0\fs24 +This software contains additional third-party software, including direct and transitive Swift Package dependencies.\ +Each project is distributed under its original license. Links below identify the license files for the resolved package revisions.\ __________________________________\ \ -\f1\b Nextcloud Kit\ +\f1\b Abseil\f0\b0\ +\ +https://github.com/google/abseil-cpp-binary\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/abseil-cpp-binary/blob/26509120768c4add61cc1a63059f15f4fb49edb4/LICENSE\ +__________________________________\ +\ -\f0\b0 \ -https://github.com/nextcloud/NextcloudKit\ +\f1\b Alamofire\f0\b0\ +\ +https://github.com/Alamofire/Alamofire\ \ -GPLv3 Licensed \ +MIT License\ +\ +Copyright (c) 2014-2022 Alamofire Software Foundation (http://alamofire.org/)\ \ -Copyright (c) Marino Faggiana\ +License: https://github.com/Alamofire/Alamofire/blob/bda9ed57d72988a3a2ada33d824583541f86eac6/LICENSE\ +__________________________________\ \ -\f1\b Alamofire - -\f0\b0 MIT License (MIT) -\f1\b \ -SwiftyXMLParser - -\f0\b0 MIT License (MIT) -\f1\b \ -SwiftyJSON - -\f0\b0 MIT License (MIT) \ +\f1\b App Check\f0\b0\ +\ +https://github.com/google/app-check\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/app-check/blob/c7eb17e5545f36e43dc50e70da09c2e999714e16/LICENSE\ __________________________________\ \ -\f1\b NYMnemonic - BIP 39 style mnemonic codes\ +\f1\b CodeScanner\f0\b0\ +\ +https://github.com/twostraws/CodeScanner\ +\ +MIT License\ +\ +Copyright (c) 2021 Paul Hudson\ +\ +License: https://github.com/twostraws/CodeScanner/blob/5e886430238944c7200fc9e10dbf2d9550dba865/LICENSE\ +__________________________________\ +\ -\f0\b0 __________________________________\ +\f1\b DropDown\f0\b0\ +\ +https://github.com/AssistoLab/DropDown\ +\ +MIT License\ +\ +Copyright (c) 2014 kevin-hirsch\ +\ +License: https://github.com/AssistoLab/DropDown/blob/2ab6f6ce19f0117d1a76ea043ef8f57722c65d16/LICENSE\ +__________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 LucidBanner\ +\f1\b EasyTipView\f0\b0\ +\ +https://github.com/marinofaggiana/EasyTipView\ \ -h -\f0\b0 ttps://github.com/marinofaggiana/LucidBanner -\f1\b \ +MIT License\ +\ +Copyright (c) 2015 Teodor Patra\u351?\ +\ +License: https://github.com/marinofaggiana/EasyTipView/blob/bb25a13c8fa5d65144c8e5b5d31237b80a001b07/LICENSE\ +__________________________________\ \ -\f0\b0 MIT License\ +\f1\b Firebase (Crashlytics and Database)\f0\b0\ +\ +https://github.com/firebase/firebase-ios-sdk\ +\ +Apache License 2.0\ +\ +License: https://github.com/firebase/firebase-ios-sdk/blob/1c35b9453868cea1ecdf9f112b5b23b8b6343854/LICENSE\ +__________________________________\ \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 -\cf0 Copyright (c) Marino Faggiana -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 -\f0\b0 \cf0 __________________________________\ +\f1\b Google Ads On Device Conversion\f0\b0\ +\ +https://github.com/googleads/google-ads-on-device-conversion-ios-sdk\ +\ +Apache License 2.0\ +\ +License: https://github.com/googleads/google-ads-on-device-conversion-ios-sdk/blob/0208e2681ec81f8a9c81084696f60fcce26cb7ee/LICENSE\ +__________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 NextcloudKit\ +\f1\b GoogleAppMeasurement\f0\b0\ \ -h -\f0\b0 ttps://github.com/nextcloud/NextcloudKit -\f1\b \ +https://github.com/google/GoogleAppMeasurement\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleAppMeasurement/blob/1a3546fb9189292e9d64db9e72add7a286c0f85a/LICENSE\ +__________________________________\ \ -\f0\b0 GPLv3 License -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardeftab720\pardirnatural\partightenfactor0 +\f1\b GoogleDataTransport\f0\b0\ +\ +https://github.com/google/GoogleDataTransport\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleDataTransport/blob/ba3358d3c3dbae8ef230b58a46b97ad65e84e974/LICENSE\ +__________________________________\ +\ -\f0\b0 \cf0 __________________________________\ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 +\f1\b GoogleUtilities\f0\b0\ +\ +https://github.com/google/GoogleUtilities\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/GoogleUtilities/blob/cc31f5ed464b111f877ce8554cbef4455d410d91/LICENSE\ +__________________________________\ +\ -\f1\b \cf0 \ -OpenSSL\ +\f1\b GRDB.swift\f0\b0\ +\ +https://github.com/groue/GRDB.swift\ +\ +MIT License\ +\ +Copyright (C) 2015-2025 Gwendal Rou\u233?\ +\ +License: https://github.com/groue/GRDB.swift/blob/b83108d10f42680d78f23fe4d4d80fc88dab3212/LICENSE\ +__________________________________\ \ -\f0\b0 https://www.openssl.org/source/license.html\ +\f1\b gRPC\f0\b0\ +\ +https://github.com/google/grpc-binary\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/grpc-binary/blob/9d5e9f05670c9eab8d8c22731ddd5d4d9cf867a8/LICENSE\ +__________________________________\ +\ -\f1\b \ +\f1\b GTM Session Fetcher\f0\b0\ +\ +https://github.com/google/gtm-session-fetcher\ +\ +Apache License 2.0\ +\ +License: https://github.com/google/gtm-session-fetcher/blob/724a52eea6329b7e12d3ad8300d76ca9f3895fcc/LICENSE\ +__________________________________\ +\ -\f0\b0 Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com)\ +\f1\b GzipSwift\f0\b0\ +\ +https://github.com/1024jp/GzipSwift\ +\ +MIT License\ +\ +\u169? 2014-2026 1024jp\ +\ +License: https://github.com/1024jp/GzipSwift/blob/41d37421fe2cbbfae4a5593dc29d12999fb73cd7/LICENSE\ __________________________________\ \ -\f1\b Realm -\f0\b0 \ +\f1\b Interop for Google SDKs\f0\b0\ +\ +https://github.com/google/interop-ios-for-google-sdks\ \ Apache License 2.0\ \ -Copyright (c) 2014-2017 https://realm.io\ +License: https://github.com/google/interop-ios-for-google-sdks/blob/040d087ac2267d2ddd4cca36c757d1c6a05fdbfe/LICENSE\ __________________________________\ \ -\f1\b KeychainAccess -\f0\b0 \ +\f1\b KeychainAccess\f0\b0\ +\ +https://github.com/kishikawakatsumi/KeychainAccess\ \ MIT License\ \ -Copyright (c) kishikawa katsumi\ +Copyright (c) 2014 kishikawa katsumi\ +\ +License: https://github.com/kishikawakatsumi/KeychainAccess/blob/84e546727d66f1adc5439debad16270d0fdd04e7/LICENSE\ __________________________________\ \ -\f1\b VLCKit -\f0\b0 \ +\f1\b LevelDB\f0\b0\ \ -GNU Lesser General Public License 2.1\ +https://github.com/firebase/leveldb\ +\ +BSD 3-Clause License\ \ -Copyright (c) VideoLAN\ +Copyright (c) 2011 The LevelDB Authors. All rights reserved.\ +\ +License: https://github.com/firebase/leveldb/blob/a0bc79961d7be727d258d33d5a6b2f1023270ba1/LICENSE\ __________________________________\ \ -\f1\b SwiftRichString -\f0\b0 \ +\f1\b LucidBanner\f0\b0\ +\ +https://github.com/marinofaggiana/LucidBanner\ \ MIT License\ \ -Copyright (c) Daniele Margutti\ +Copyright (c) 2025 Marino Faggiana\ +\ +License: https://github.com/marinofaggiana/LucidBanner/blob/408af8b2ff2451f055ff17042d8b9fb5c822d5fd/LICENSE\ __________________________________\ \ -\f1\b QRCodeReader -\f0\b0 \ +\f1\b Mantis\f0\b0\ +\ +https://github.com/marinofaggiana/Mantis\ \ MIT License\ \ -Copyright (c) Yannick Loriot\ +Copyright (c) 2018 Yingtao Guo\ +\ +License: https://github.com/marinofaggiana/Mantis/blob/d145ce2403e0d94d4926eb8510bc4966fbf1bfca/LICENSE\ __________________________________\ \ -\f1\b Parchment -\f0\b0 \ +\f1\b MarkdownKit\f0\b0\ +\ +https://github.com/bmoliveira/MarkdownKit\ \ MIT License\ \ -Copyright (c) Martin Rechsteiner\ +Copyright (c) 2016 Ivan Bruel\ +\ +License: https://github.com/bmoliveira/MarkdownKit/blob/39ea7a3fc7e65bcafe55152592b834859be574a5/LICENSE\ __________________________________\ \ -\f1\b DropDown -\f0\b0 \ +\f1\b nanopb\f0\b0\ \ -MIT License\ +https://github.com/firebase/nanopb\ \ -Copyright (c) Kevin Hirsch\ +zlib License\ +\ +Copyright (c) 2011 Petteri Aimonen \ +\ +License: https://github.com/firebase/nanopb/blob/3851d94a41890dea16dc3db34caf60e585cb4163/LICENSE.txt\ __________________________________\ \ -\f1\b TLPhotoPicker -\f0\b0 \ +\f1\b NextcloudKit\f0\b0\ \ -MIT License\ +https://github.com/nextcloud/NextcloudKit\ +\ +GNU General Public License v3.0 or later, with Apple App Store exception (COPYING.iOS)\ +\ +Copyright Nextcloud GmbH, Marino Faggiana and Nextcloud contributors\ \ -Copyright (c) wade.hawk, junhyi.park@gmail.com\ +License: https://github.com/nextcloud/NextcloudKit/blob/705e69cf0526cac0bdeaf21699c41c895cae42a0/LICENSE.txt\ __________________________________\ \ -\f1\b MarkdownKit -\f0\b0 \ +\f1\b NYMnemonic - BIP 39 style mnemonic codes\f0\b0\ \ MIT License\ +\ +Copyright (c) 2014 Nybex, Inc. (https://nybex.com)\ __________________________________\ \ -\f1\b Firebase Crashlytics -\f0\b0 \ +\f1\b OpenSSL\f0\b0\ +\ +https://github.com/krzyzanowskim/OpenSSL\ \ Apache License 2.0\ \ -Copyright (c) Google Inc.\ +License: https://github.com/krzyzanowskim/OpenSSL/blob/0a6149bff4fb00921ce0ec1811f726b166638095/LICENSE.txt\ __________________________________\ \ -\f1\b Queuer -\f0\b0 \ +\f1\b PopupView\f0\b0\ +\ +https://github.com/exyte/PopupView\ \ MIT License\ \ -Copyright (c) Fabrizio Brancati\ +Copyright (c) 2019 exyte \ +\ +License: https://github.com/exyte/PopupView/blob/99695337ce9e965b70e4aa14b49b1a9920017333/LICENSE\ __________________________________\ \ -\f1\b TOPasscodeViewController -\f0\b0 \ +\f1\b Promises\f0\b0\ \ -MIT License\ +https://github.com/google/promises\ +\ +Apache License 2.0\ \ -Copyright (c) Tim Oliver\ +License: https://github.com/google/promises/blob/f4a19a3c313dc2616c70bb49d29a799fb16be837/LICENSE\ __________________________________\ \ -\f1\b EasyTipView -\f0\b0 \ +\f1\b QRCodeReader.swift\f0\b0\ +\ +https://github.com/yannickl/QRCodeReader.swift\ \ MIT License\ \ -Copyright (c) Teodor Patras\ +Copyright (c) 2014-present Yannick Loriot\ +\ +License: https://github.com/yannickl/QRCodeReader.swift/blob/5020b5a47199d8ba80c83a4b4fafd70e9dc9dc7f/LICENSE\ __________________________________\ \ -\f1\b Swifter -\f0\b0 \ +\f1\b Realm Core\f0\b0\ \ -BSD 3\ +https://github.com/realm/realm-core\ \ -Copyright (c) Damian Kolakowski\ +Apache License 2.0\ +\ +License: https://github.com/realm/realm-core/blob/b4192c46305570577c4d08df790fddec5ab3aa04/LICENSE\ __________________________________\ \ -\f1\b Mantis -\f0\b0 \ +\f1\b Realm Swift\f0\b0\ \ -MIT License\ +https://github.com/realm/realm-swift\ \ -Copyright (c) Yingtao Guo\ +Apache License 2.0\ +\ +License: https://github.com/realm/realm-swift/blob/a40ced79265b644a896e393b0f3a37e7916e7de3/LICENSE\ __________________________________\ \ -\f1\b TagListView -\f0\b0 \ +\f1\b Swift Atomics\f0\b0\ +\ +https://github.com/apple/swift-atomics\ +\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-atomics/blob/0442cb5a3f98ab802acb777929fdb446bda11a34/LICENSE.txt\ +__________________________________\ +\ + +\f1\b Swift Collections\f0\b0\ +\ +https://github.com/apple/swift-collections\ +\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-collections/blob/35a1edcb8ebd2bfe251d587f0b69e62af28f508c/LICENSE.txt\ +__________________________________\ +\ + +\f1\b Swift System\f0\b0\ +\ +https://github.com/apple/swift-system\ +\ +Apache License 2.0 with Swift Runtime Library Exception\ +\ +License: https://github.com/apple/swift-system/blob/869129b7bf4ecc57b97d0193ad29690ca2134750/LICENSE.txt\ +__________________________________\ +\ + +\f1\b Swifter\f0\b0\ +\ +https://github.com/httpswift/swifter\ +\ +BSD 3-Clause License\ +\ +Copyright (c) 2014, Damian Ko\u322?akowski\ +\ +License: https://github.com/httpswift/swifter/blob/1e4f51c92d7ca486242d8bf0722b99de2c3531aa/LICENSE\ +__________________________________\ +\ + +\f1\b SwiftNIO\f0\b0\ +\ +https://github.com/apple/swift-nio\ +\ +Apache License 2.0\ +\ +License: https://github.com/apple/swift-nio/blob/6db9d9abd92ad7399b8a564cafbb87d3507245fa/LICENSE.txt\ +__________________________________\ +\ + +\f1\b SwiftNIO SSL\f0\b0\ +\ +https://github.com/apple/swift-nio-ssl\ +\ +Apache License 2.0\ +\ +License: https://github.com/apple/swift-nio-ssl/blob/03827c1a9fdb2b6b00a4e93ede8861520263af8c/LICENSE.txt\ +__________________________________\ +\ + +\f1\b SwiftRichString\f0\b0\ +\ +https://github.com/malcommac/SwiftRichString\ \ MIT License\ \ -Copyright (c) LIU Dongyuan\ +Copyright (c) 2018 Daniele Margutti\ +\ +License: https://github.com/malcommac/SwiftRichString/blob/9bf4b5af6bb4386865636fc504d6c588c2b65040/LICENSE\ __________________________________\ \ -\pard\pardeftab720\partightenfactor0 -\f1\b \cf0 GzipSwift\ +\f1\b SwiftyJSON\f0\b0\ +\ +https://github.com/SwiftyJSON/SwiftyJSON\ +\ +MIT License\ +\ +Copyright (c) 2017 Ruoyu Fu\ +\ +License: https://github.com/SwiftyJSON/SwiftyJSON/blob/af76cf3ef710b6ca5f8c05f3a31307d44a3c5828/LICENSE\ +__________________________________\ +\ -\f0\b0 \ +\f1\b SwiftyXMLParser\f0\b0\ +\ +https://github.com/yahoojapan/SwiftyXMLParser\ +\ MIT License\ \ -Copyright (c) 1024jp -\f1\b \ -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\partightenfactor0 +Copyright (c) 2016 Yahoo Japan Corporation\ +\ +License: https://github.com/yahoojapan/SwiftyXMLParser/blob/d7a1d23f04c86c1cd2e8f19247dd15d74e0ea8be/LICENSE\ +__________________________________\ +\ -\f0\b0 \cf0 __________________________________\ +\f1\b TagListView\f0\b0\ +\ +https://github.com/ElaWorkshop/TagListView\ +\ +MIT License\ +\ +Copyright (c) 2015 LIU Dongyuan\ +\ +License: https://github.com/ElaWorkshop/TagListView/blob/b30b61826736f4d9ec04aef5df891f3914423f16/LICENSE\ +__________________________________\ \ -\f1\b LRUCache -\f0\b0 \ +\f1\b TLPhotoPicker\f0\b0\ +\ +https://github.com/tilltue/TLPhotoPicker\ \ MIT License\ \ -Copyright (c) Nick Lockwood\ +Copyright (c) 2017 wade.hawk \ +\ +License: https://github.com/tilltue/TLPhotoPicker/blob/91d7bca65721928af30c6a2bb1a1f517f7862964/LICENSE\ __________________________________\ -} \ No newline at end of file +\ + +\f1\b VLCKit (MobileVLCKit)\f0\b0\ +\ +https://code.videolan.org/videolan/VLCKit\ +\ +GNU Lesser General Public License 2.1\ +\ +Copyright VideoLAN\ +\ +License: https://code.videolan.org/videolan/VLCKit/-/blob/master/COPYING\ +__________________________________\ +\ +} diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index 8493b99bc9..1aacb0371a 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -5,11 +5,9 @@ import UIKit import SwiftUI import NextcloudKit -import LocalAuthentication @MainActor -class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscodeViewControllerDelegate { - var passcodeType = "" +class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling { let preference = NCPreferences() let networkingE2EE = NCNetworkingE2EE() @@ -101,38 +99,27 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode // MARK: - Passcode + /// Authenticate before performing the selected E2EE action, including in Debug builds. @objc func requestPasscodeType(_ passcodeType: String) { - #if DEBUG - self.passcodeType = passcodeType - correctPasscode() - return - #else - let laContext = LAContext() - var error: NSError? - let passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) - - passcodeViewController.delegate = self - passcodeViewController.keypadButtonShowLettering = false - if NCPreferences().touchFaceID, laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - if error == nil { - if laContext.biometryType == .faceID { - passcodeViewController.biometryType = .faceID - passcodeViewController.allowBiometricValidation = true - } else if laContext.biometryType == .touchID { - passcodeViewController.biometryType = .touchID - } - passcodeViewController.allowBiometricValidation = true - passcodeViewController.automaticallyPromptForBiometricValidation = true + guard let presenter = controller?.topMostViewController() else { return } + let account = session.account + NCPasscodeView.present(from: presenter, allowCancel: true) { [weak self] result in + guard let self else { return } + switch result { + case .authenticated: + guard self.session.account == account, + NCManageDatabase.shared.getTableAccount(account: account) != nil else { return } + self.performAuthenticatedAction(passcodeType) + case .reset: + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + case .cancelled, .configured, .disabled: + break } } - - self.passcodeType = passcodeType - controller?.topMostViewController().present(passcodeViewController, animated: true) - #endif } - @objc func correctPasscode() { - switch self.passcodeType { + private func performAuthenticatedAction(_ passcodeType: String) { + switch passcodeType { case "startE2E": Task { do { @@ -177,32 +164,4 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode break } } - - nonisolated func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode code: String) -> Bool { - if code == NCPreferences().passcode { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.correctPasscode() - } - return true - } else { - return false - } - } - - nonisolated func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, _ in - if success { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - passcodeViewController.dismiss(animated: true) - self.correctPasscode() - } - } - } - } - - nonisolated func didTapCancel(in passcodeViewController: TOPasscodeViewController) { - Task {@MainActor in - passcodeViewController.dismiss(animated: true) - } - } } diff --git a/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift b/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift index 7f770337f2..bd466b61fa 100644 --- a/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift +++ b/iOSClient/Settings/Helpers/NCAcknowledgementsView.swift @@ -41,6 +41,7 @@ struct NCAcknowledgementsView: View { .foregroundColor(Color(NCBrandColor.shared.iconImageColor)) } } + .accessibilityIdentifier("settings.acknowledgements.close") .padding() } Divider() diff --git a/iOSClient/Settings/Settings/NCSettingsView.swift b/iOSClient/Settings/Settings/NCSettingsView.swift index b89f0e40ad..9aa1bbc292 100644 --- a/iOSClient/Settings/Settings/NCSettingsView.swift +++ b/iOSClient/Settings/Settings/NCSettingsView.swift @@ -53,6 +53,7 @@ struct NCSettingsView: View { } }) .tint(Color(NCBrandColor.shared.textColor)) + .accessibilityIdentifier("settings.passcode.toggle") .disabled(NCBrandOptions.shared.enforce_passcode_lock) }, header: { Text(NSLocalizedString("_privacy_", comment: "")) @@ -77,6 +78,7 @@ struct NCSettingsView: View { .tint(Color(NCBrandColor.shared.textColor)) } }) + .accessibilityIdentifier("settings.passcode.change") // Enable Touch ID Toggle(NSLocalizedString("_enable_touch_face_id_", comment: ""), isOn: $model.enableTouchFaceID) .font(.body) @@ -95,6 +97,7 @@ struct NCSettingsView: View { // Reset app wrong attempts Toggle(NSLocalizedString("_reset_wrong_passcode_option_", comment: ""), isOn: $model.resetWrongAttempts) + .accessibilityIdentifier("settings.passcode.reset") .font(.body) .onChange(of: model.resetWrongAttempts) { model.updateResetWrongAttemptsSetting() @@ -306,10 +309,16 @@ struct NCSettingsView: View { }) } .sheet(isPresented: $showPasscode) { - SetupPasscodeView(isLockActive: $model.isLockActive, controller: model.controller) + NCPasscodeView(mode: NCPreferences().passcode == nil ? .create : .disable, + allowCancel: true, + windowScene: SceneManager.shared.getWindowScene(controller: model.controller), + completion: completePasscode) } .sheet(isPresented: $showChangePasscode) { - SetupPasscodeView(isLockActive: $model.isLockActive, controller: model.controller, changePasscode: true) + NCPasscodeView(mode: .change, + allowCancel: true, + windowScene: SceneManager.shared.getWindowScene(controller: model.controller), + completion: completePasscode) } #if DEBUG .alert("Database export failed", isPresented: $showDatabaseExportError) { @@ -322,6 +331,22 @@ struct NCSettingsView: View { .defaultViewModifier(model) } + /// Update the lock setting only after the passcode operation has completed. + private func completePasscode(_ result: NCPasscodeModel.Result) { + switch result { + case .configured: + model.isLockActive = true + case .disabled: + model.isLockActive = false + case .reset: + (UIApplication.shared.delegate as? AppDelegate)?.resetApplication() + case .authenticated, .cancelled: + break + } + showPasscode = false + showChangePasscode = false + } + #if DEBUG private func exportDebugDatabase() { isExportingDatabase = true diff --git a/iOSClient/Settings/Settings/SetupPasscodeView.swift b/iOSClient/Settings/Settings/SetupPasscodeView.swift deleted file mode 100644 index b04c7be5cd..0000000000 --- a/iOSClient/Settings/Settings/SetupPasscodeView.swift +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2026 Milen Pivchev -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import Foundation -import UIKit -import SwiftUI -import LocalAuthentication -import PopupView - -struct SetupPasscodeView: UIViewControllerRepresentable { - var isPasscodeReset: Bool { - let passcodeCounterFailReset = NCPreferences().passcodeCounterFailReset - return NCPreferences().resetAppCounterFail && passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts - } - - var isPasscodeCounterFail: Bool { - let passcodeCounterFail = NCPreferences().passcodeCounterFail - return passcodeCounterFail > 0 && passcodeCounterFail >= 3 - } - - @Binding var isLockActive: Bool - weak var controller: NCMainTabBarController? - var changePasscode: Bool = false - - func makeUIViewController(context: Context) -> UIViewController { - let laContext = LAContext() - var error: NSError? - let viewController: UIViewController - - if !NCPreferences().passcode.isEmptyOrNil, !changePasscode { - let passcodeVC = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) - passcodeVC.keypadButtonShowLettering = false - - if NCPreferences().touchFaceID, laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error), error == nil { - switch laContext.biometryType { - case .faceID: - passcodeVC.biometryType = .faceID - case .touchID: - passcodeVC.biometryType = .touchID - default: - print("No Biometric support") - } - passcodeVC.allowBiometricValidation = true - passcodeVC.automaticallyPromptForBiometricValidation = true - } - - passcodeVC.delegate = context.coordinator - viewController = passcodeVC - } else { - let passcodeSettingsVC = TOPasscodeSettingsViewController() - passcodeSettingsVC.hideOptionsButton = true - passcodeSettingsVC.requireCurrentPasscode = changePasscode - passcodeSettingsVC.passcodeType = .sixDigits - passcodeSettingsVC.delegate = context.coordinator - viewController = passcodeSettingsVC - } - - return PasscodeContainerViewController(child: viewController) - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) { - // Update the view controller if needed - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, @MainActor TOPasscodeSettingsViewControllerDelegate, @MainActor TOPasscodeViewControllerDelegate { - var parent: SetupPasscodeView - init(_ parent: SetupPasscodeView) { - self.parent = parent - } - - func didPerformBiometricValidationRequest(in passcodeViewController: TOPasscodeViewController) { - let context = LAContext() - var error: NSError? - - if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: NCBrandOptions.shared.brand) { success, evaluateError in - DispatchQueue.main.async { - if success { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in - parent.isLockActive = false - NCPreferences().passcode = nil - - passcodeViewController.dismiss(animated: true) - } - } else if evaluateError != nil { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - } - } - } - } - } - - // This triggers only for "Change passcode" option - @MainActor - func passcodeSettingsViewController(_ passcodeSettingsViewController: TOPasscodeSettingsViewController, didAttemptCurrentPasscode passcode: String) -> Bool { - if passcode == NCPreferences().passcode { - return true - } else { - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - } - - if parent.isPasscodeCounterFail { - UIAlertController.failedPasscode(presenter: passcodeSettingsViewController) - } - - return false - } - - func passcodeSettingsViewController(_ passcodeSettingsViewController: TOPasscodeSettingsViewController, didChangeToNewPasscode passcode: String, of type: TOPasscodeType) { - NCPreferences().passcode = passcode - parent.isLockActive = true - passcodeSettingsViewController.dismiss(animated: true) - } - - func didTapCancel(in passcodeViewController: TOPasscodeViewController) { - passcodeViewController.dismiss(animated: true) - } - - // This triggers upon pressing "Lock: On" - @MainActor - func passcodeViewController(_ passcodeViewController: TOPasscodeViewController, isCorrectCode passcode: String) -> Bool { - if passcode == NCPreferences().passcode { - parent.isLockActive = false - NCPreferences().passcode = nil - return true - } - - NCPreferences().passcodeCounterFail += 1 - NCPreferences().passcodeCounterFailReset += 1 - - if parent.isPasscodeCounterFail { - UIAlertController.failedPasscode(presenter: passcodeViewController) - } - - return false - } - } -} - -private final class PasscodeContainerViewController: UIViewController { - private let child: UIViewController - - init(child: UIViewController) { - self.child = child - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - - addChild(child) - child.view.frame = view.bounds - child.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.addSubview(child.view) - child.didMove(toParent: self) - - NotificationCenter.default.addObserver(self, - selector: #selector(presentFailedPasscodeIfNeeded), - name: UIApplication.didBecomeActiveNotification, - object: nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - presentFailedPasscodeIfNeeded() - } - - @objc private func presentFailedPasscodeIfNeeded() { - guard presentedViewController == nil, NCPreferences().passcodeCounterFail >= 3 else { return } - - UIAlertController.failedPasscode(presenter: self) - } -} diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 4823b05018..6dd675d09c 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -879,3 +879,6 @@ "_seek_backward_10_" = "Go back 10 seconds"; "_seek_forward_10_" = "Go forward 10 seconds"; + +"_passcode_incorrect_" = "Incorrect passcode. Try again."; +"_passcode_digits_entered_" = "Passcode digits entered"; diff --git a/iOSClient/Utility/NCAskAuthorization.swift b/iOSClient/Utility/NCAskAuthorization.swift index a58f97c3d8..62596ded33 100644 --- a/iOSClient/Utility/NCAskAuthorization.swift +++ b/iOSClient/Utility/NCAskAuthorization.swift @@ -70,9 +70,6 @@ class NCAskAuthorization: NSObject { self.isRequesting = true PHPhotoLibrary.requestAuthorization { allowed in self.isRequesting = false -#if !EXTENSION - // DispatchQueue.main.async { NCPasscode.shared.hidePrivacyProtectionWindow() } -#endif if allowed == PHAuthorizationStatus.authorized { completion(true) diff --git a/iOSClient/Utility/Passcode/NCPasscodeModel.swift b/iOSClient/Utility/Passcode/NCPasscodeModel.swift new file mode 100644 index 0000000000..0aaae1392c --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeModel.swift @@ -0,0 +1,233 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import LocalAuthentication + +/// Owns one passcode operation; persistent protection is shared through Keychain. +@MainActor +final class NCPasscodeModel: ObservableObject { + enum Mode { + case authenticate, create, change, disable + } + + enum Result: Equatable { + case authenticated, configured, disabled, cancelled, reset + } + + private enum Step { + case authenticate, enterNew, confirmNew + } + + let mode: NCPasscodeModel.Mode + let allowCancel: Bool + @Published private(set) var digits = "" + @Published private(set) var message = "" + @Published private(set) var lockoutEnd: Date? + @Published private(set) var isAuthenticating = false + @Published private(set) var result: NCPasscodeModel.Result? + @Published private var step: Step + var onCompletion: (@MainActor (NCPasscodeModel.Result) -> Void)? + + static var isPasscodeReset: Bool { + NCPreferences().resetAppCounterFail && NCPreferences().passcodeCounterFailReset >= NCBrandOptions.shared.resetAppPasscodeAttempts + } + + private var isPreview = false + private var newPasscode = "" + private var biometricContext: LAContext? + private var biometricRequest = UUID() + private var didStart = false + private let preferences = NCPreferences() + + init(mode: NCPasscodeModel.Mode, allowCancel: Bool) { + self.mode = mode + self.allowCancel = allowCancel + self.step = mode == .create ? .enterNew : .authenticate + } + +#if DEBUG + /// Preview data never reads or changes stored credentials or starts biometrics. + static func preview(mode: NCPasscodeModel.Mode = .authenticate, locked: Bool = false) -> NCPasscodeModel { + let model = NCPasscodeModel(mode: mode, allowCancel: mode != .authenticate) + model.isPreview = true + model.digits = locked ? "" : "123" + model.lockoutEnd = locked ? Date().addingTimeInterval(60) : nil + return model + } +#endif + + var title: String { + switch step { + case .authenticate: return "Enter Passcode" + case .enterNew: return "Enter a new passcode" + case .confirmNew: return "Confirm new passcode" + } + } + + var biometricSymbol: String? { + if isPreview { return step == .authenticate ? "faceid" : nil } + guard step == .authenticate, mode != .change, preferences.touchFaceID, preferences.passcode != nil else { return nil } + let context = LAContext() + guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) else { return nil } + return context.biometryType == .faceID ? "faceid" : "touchid" + } + + var canEnterCode: Bool { + result == nil && lockoutEnd == nil && !isAuthenticating + } + + func start() async { + guard !isPreview else { return } + refreshProtection() + guard !didStart else { return } + didStart = true + await authenticateWithBiometrics() + } + + /// Re-read shared counters on foreground entry and while the lockout is visible. + func refreshProtection() { + guard !isPreview else { return } + guard result == nil else { return } + if let deadline = preferences.passcodeLockoutEnd, deadline <= Date() { + // Expiring the retry delay must preserve the total used for the app-reset threshold. + preferences.passcodeCounterFail = 0 + preferences.passcodeLockoutEnd = nil + } + if Self.isPasscodeReset { + finish(.reset) + return + } + if preferences.passcodeCounterFail >= 3 && preferences.passcodeLockoutEnd == nil { + preferences.passcodeLockoutEnd = Date().addingTimeInterval(TimeInterval(NCBrandOptions.shared.passcodeSecondsFail)) + } + lockoutEnd = preferences.passcodeLockoutEnd + if lockoutEnd != nil { + digits = "" + cancelBiometrics() + } + } + + func append(_ digit: Int) { + refreshProtection() + guard canEnterCode, (0...9).contains(digit), digits.count < 6 else { return } + message = "" + digits += String(digit) + if digits.count == 6 && !isPreview { + submit() + } + } + + func deleteDigit() { + guard canEnterCode, !digits.isEmpty else { return } + digits.removeLast() + } + + func cancel() { + guard allowCancel else { return } + finish(.cancelled) + } + + func cancelBiometrics() { + biometricRequest = UUID() + biometricContext?.invalidate() + biometricContext = nil + isAuthenticating = false + } + + func authenticateWithBiometrics() async { + guard !isPreview else { return } + refreshProtection() + guard canEnterCode, biometricSymbol != nil else { return } + let context = LAContext() + let request = UUID() + biometricRequest = request + biometricContext = context + isAuthenticating = true + defer { + if biometricRequest == request { + biometricContext = nil + isAuthenticating = false + } + } + do { + let success = try await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, + localizedReason: NCBrandOptions.shared.brand) + guard biometricRequest == request, result == nil else { return } + refreshProtectionAfterBiometrics(success: success) + } catch { + guard biometricRequest == request, result == nil else { return } + // Cancellation and choosing the keypad are not failed authentication attempts. + if (error as? LAError)?.code == .authenticationFailed { + registerFailure() + } + } + } + + private func refreshProtectionAfterBiometrics(success: Bool) { + refreshProtection() + guard result == nil, lockoutEnd == nil else { return } + if success { + authenticated() + } + } + + private func submit() { + let code = digits + digits = "" + switch step { + case .authenticate: + guard let saved = preferences.passcode, code == saved else { + registerFailure() + return + } + authenticated() + case .enterNew: + newPasscode = code + step = .confirmNew + case .confirmNew: + guard code == newPasscode else { + newPasscode = "" + step = .enterNew + message = "Passcodes didn't match. Try again." + return + } + preferences.passcode = code + preferences.clearPasscodeFailures() + finish(.configured) + } + } + + private func authenticated() { + preferences.clearPasscodeFailures() + switch mode { + case .change: + step = .enterNew + case .disable: + guard !NCBrandOptions.shared.enforce_passcode_lock else { return } + preferences.passcode = nil + finish(.disabled) + case .authenticate: + finish(.authenticated) + case .create: + break + } + } + + private func registerFailure() { + preferences.passcodeCounterFail += 1 + preferences.passcodeCounterFailReset += 1 + message = "_passcode_incorrect_" + refreshProtection() + } + + private func finish(_ result: NCPasscodeModel.Result) { + guard self.result == nil else { return } + self.result = result + digits = "" + newPasscode = "" + cancelBiometrics() + onCompletion?(result) + } +} diff --git a/iOSClient/Utility/Passcode/NCPasscodeView.swift b/iOSClient/Utility/Passcode/NCPasscodeView.swift new file mode 100644 index 0000000000..5aecd1f662 --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeView.swift @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +/// Shared keypad for authentication, creation, change and removal of the app code. +@MainActor +struct NCPasscodeView: View { + @StateObject private var model: NCPasscodeModel + private weak var windowScene: UIWindowScene? + + init(model: NCPasscodeModel, windowScene: UIWindowScene? = nil) { + _model = StateObject(wrappedValue: model) + self.windowScene = windowScene + } + + /// SwiftUI callers handle the result and dismiss their sheet in completion. + init(mode: NCPasscodeModel.Mode = .authenticate, allowCancel: Bool = false, + windowScene: UIWindowScene? = nil, completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + _model = StateObject(wrappedValue: { + let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) + model.onCompletion = completion + return model + }()) + self.windowScene = windowScene + } + + /// UIKit callers receive the result after the passcode screen has closed. + static func present(from presenter: UIViewController, mode: NCPasscodeModel.Mode = .authenticate, + allowCancel: Bool = false, completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + guard !(presenter is UIHostingController) else { return } + var top = presenter + while let presented = top.presentedViewController { + if presented is UIHostingController { return } + top = presented + } + let model = NCPasscodeModel(mode: mode, allowCancel: allowCancel) + let controller = UIHostingController(rootView: NCPasscodeView(model: model, windowScene: top.view.window?.windowScene)) + controller.modalPresentationStyle = allowCancel ? .pageSheet : .fullScreen + controller.isModalInPresentation = !allowCancel + model.onCompletion = { [weak controller] result in + guard let controller else { return } + if controller.isBeingDismissed || controller.presentingViewController == nil { + completion(result) + } else { + controller.dismiss(animated: allowCancel && controller.view.window?.windowScene?.activationState == .foregroundActive) { + completion(result) + } + } + } + top.present(controller, animated: allowCancel) + } + + var body: some View { + NavigationStack { + GeometryReader { geometry in + ScrollView { + VStack(spacing: 24) { + Image(systemName: "lock.shield") + .font(.largeTitle) + .accessibilityHidden(true) + Text(NSLocalizedString(model.title, comment: "")) + .font(.title2) + .accessibilityIdentifier("passcode.title") + .multilineTextAlignment(.center) + .accessibilityAddTraits(.isHeader) + HStack(spacing: 16) { + ForEach(0..<6) { index in + Image(systemName: index < model.digits.count ? "circle.fill" : "circle") + .font(.body) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(NSLocalizedString("_passcode_digits_entered_", comment: "")) + .accessibilityValue("\(model.digits.count) / 6") + .accessibilityIdentifier("passcode.progress") + + if let deadline = model.lockoutEnd { + TimelineView(.periodic(from: .now, by: 1)) { timeline in + VStack(spacing: 8) { + Text(NSLocalizedString("_passcode_counter_fail_", comment: "")) + Text("\(max(0, Int(deadline.timeIntervalSince(timeline.date).rounded(.up)))) " + NSLocalizedString("_seconds_", comment: "")) + .accessibilityIdentifier("passcode.lockout.remaining") + } + .multilineTextAlignment(.center) + } + } else { + Text(NSLocalizedString(model.message, comment: "")) + .accessibilityIdentifier("passcode.error") + .foregroundStyle(.red) + .multilineTextAlignment(.center) + .frame(minHeight: 24) + } + keypad + } + .padding(24) + .frame(maxWidth: 360) + .frame(maxWidth: .infinity, minHeight: geometry.size.height) + } + } + .background(Color(uiColor: .systemBackground)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if model.allowCancel { + ToolbarItem(placement: .cancellationAction) { + Button { + model.cancel() + } label: { + Image(systemName: "xmark") + } + .accessibilityLabel(NSLocalizedString("_close_", comment: "")) + .accessibilityIdentifier("passcode.close") + } + } + } + } + .interactiveDismissDisabled(!model.allowCancel) + .task { + if let windowScene, windowScene.activationState != .foregroundActive { + model.refreshProtection() + } else { + await model.start() + } + } + .task(id: model.lockoutEnd) { + guard let deadline = model.lockoutEnd else { return } + let remaining = max(0, deadline.timeIntervalSinceNow) + do { + try await Task.sleep(for: .seconds(remaining)) + model.refreshProtection() + } catch { return } + } + .onReceive(NotificationCenter.default.publisher(for: UIScene.didActivateNotification)) { notification in + guard let windowScene, (notification.object as? UIScene) === windowScene else { return } + Task { await model.start() } + } + .onReceive(NotificationCenter.default.publisher(for: UIScene.didEnterBackgroundNotification)) { notification in + guard let windowScene, (notification.object as? UIScene) === windowScene else { return } + model.cancelBiometrics() + // Optional operations cancel themselves; mandatory app authentication stays open. + model.cancel() + } + .onDisappear { + model.cancelBiometrics() + model.cancel() + } + } + + private var keypad: some View { + Grid(horizontalSpacing: 20, verticalSpacing: 12) { + ForEach(0..<3) { row in + GridRow { + ForEach(1..<4) { column in + digitButton(row * 3 + column) + } + } + } + GridRow { + Button { + Task { await model.authenticateWithBiometrics() } + } label: { + Image(systemName: model.biometricSymbol ?? "faceid") + .font(.title) + .frame(minWidth: 64, minHeight: 64) + } + .accessibilityLabel(model.biometricSymbol == "touchid" ? "Touch ID" : "Face ID") + .disabled(!model.canEnterCode || model.biometricSymbol == nil) + .opacity(model.biometricSymbol == nil ? 0 : 1) + digitButton(0) + Button { + model.deleteDigit() + } label: { + Image(systemName: "delete.left") + .font(.title2) + .frame(minWidth: 64, minHeight: 64) + } + .accessibilityLabel(NSLocalizedString("_delete_", comment: "")) + .accessibilityIdentifier("passcode.delete") + .disabled(!model.canEnterCode || model.digits.isEmpty) + } + } + .buttonStyle(.plain) + } + + private func digitButton(_ digit: Int) -> some View { + Button { + model.append(digit) + } label: { + Text(String(digit)) + .font(.largeTitle.monospacedDigit()) + .frame(minWidth: 64, minHeight: 64) + .background(Color(uiColor: .secondarySystemBackground), in: Circle()) + } + .accessibilityIdentifier("passcode.digit.\(digit)") + .disabled(!model.canEnterCode) + } +} + +#if DEBUG +#Preview("Unlock") { + NCPasscodeView(model: .preview()) +} + +#Preview("Unlock – Dark") { + NCPasscodeView(model: .preview()) + .preferredColorScheme(.dark) +} + +#Preview("Create passcode") { + NCPasscodeView(model: .preview(mode: .create)) +} + +#Preview("Too many attempts") { + NCPasscodeView(model: .preview(locked: true)) +} +#endif diff --git a/iOSClient/Utility/Passcode/NCPasscodeWindow.swift b/iOSClient/Utility/Passcode/NCPasscodeWindow.swift new file mode 100644 index 0000000000..09bb0b4cb9 --- /dev/null +++ b/iOSClient/Utility/Passcode/NCPasscodeWindow.swift @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI + +/// Owns the mandatory lock above one scene's app window, independently of modal presentations. +@MainActor +final class NCPasscodeWindow: UIWindow { + private weak var mainWindow: UIWindow? + + init(mainWindow: UIWindow, windowScene: UIWindowScene, + completion: @escaping @MainActor (NCPasscodeModel.Result) -> Void) { + self.mainWindow = mainWindow + super.init(windowScene: windowScene) + // Above the app's banners, PopupView windows and privacy screen. + windowLevel = .alert + 2 + backgroundColor = .systemBackground + overrideUserInterfaceStyle = mainWindow.overrideUserInterfaceStyle + accessibilityIdentifier = "passcode.window" + accessibilityViewIsModal = true + + let model = NCPasscodeModel(mode: .authenticate, allowCancel: false) + let controller = UIHostingController(rootView: NCPasscodeView(model: model, windowScene: windowScene)) + controller.isModalInPresentation = true + rootViewController = controller + model.onCompletion = { [weak self] result in + guard let self, result == .authenticated || result == .reset else { return } + self.hide() + completion(result) + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + /// Show immediately without presenting or dismissing any controller in the app window. + func show() { + makeKeyAndVisible() + } + + /// Release the lock and restore focus to the owning app window when its scene is active. + func hide() { + let restoreFocus = isKeyWindow && windowScene?.activationState == .foregroundActive + isHidden = true + rootViewController = nil + if restoreFocus { + mainWindow?.makeKey() + } + } +} diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h deleted file mode 100644 index 6000f1fca6..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// TOPasscodeCircleImage.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -NS_ASSUME_NONNULL_BEGIN - -/** - A subclass of `UIImage` that can procedurally generate both hollow and full circle graphics at any size. - These are used for the 'normal' and 'tapped' states of the passcode circle buttons. - */ -@interface TOPasscodeCircleImage : UIImage - -/** - Generates and returns a `UIImage` of a filled circle at the specified size. - - @param size The diameter of the final circle image - @param inset An inset value that will shrink the size of the circle. This is so it can be overlaid on a hollow circle - without interfering with the anti-aliasing on the outer border. - @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. - Setting this value will increase the dimensions of the final `UIImage`. - @param antialias Whether the circle boundary will be antialiased (Since antialiasing is unnecessary if this circle will overlay another.) - */ -+ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias; - -/** - Generates and returns a `UIImage` of a hollow circle at the specified size. - - @param size The diameter of the final circle image - @param strokeWidth The thickness, in points, of the stroke making up the circle image. - @param padding External padding around the circle to ensure it won't be clipped by the edge of the layer. - Setting this value will increase the dimensions of the final `UIImage`. - */ -+ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m deleted file mode 100644 index 4288a33092..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeCircleImage.m +++ /dev/null @@ -1,75 +0,0 @@ -// -// TOPasscodeCircleImage.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeCircleImage.h" - -@implementation TOPasscodeCircleImage - -+ (UIImage *)circleImageOfSize:(CGFloat)size inset:(CGFloat)inset padding:(CGFloat)padding antialias:(BOOL)antialias -{ - UIImage *image = nil; - CGSize imageSize = (CGSize){size + (padding * 2), size + (padding * 2)}; - - UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0.0f); - { - CGContextRef context = UIGraphicsGetCurrentContext(); - - if (!antialias) { - CGContextSetShouldAntialias(context, NO); - } - - CGRect rect = (CGRect){padding + inset, padding + inset, size - (inset * 2), size - (inset * 2)}; - UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect]; - [[UIColor blackColor] setFill]; - [ovalPath fill]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -+ (UIImage *)hollowCircleImageOfSize:(CGFloat)size strokeWidth:(CGFloat)strokeWidth padding:(CGFloat)padding -{ - UIImage *image = nil; - CGSize canvasSize = (CGSize){size + (padding * 2), size + (padding * 2)}; - CGSize circleSize = (CGSize){size, size}; - - UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); - { - CGRect circleRect = (CGRect){{padding, padding}, circleSize}; - circleRect = CGRectInset(circleRect, (strokeWidth * 0.5f), (strokeWidth * 0.5f)); - - UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:circleRect]; - [[UIColor blackColor] setStroke]; - path.lineWidth = strokeWidth; - [path stroke]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h deleted file mode 100644 index 97191fd6ae..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.h +++ /dev/null @@ -1,100 +0,0 @@ -// -// TOPasscodeViewContentLayout.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import - -/** - Depending on the width of the application window, all of the content views in - the passcode view need to be resized in order to fit in the available space. - - This means that not only does the spacing and sizing of views need to be changed, but - image assets need to be regenerated and font sizes need to change as well. - - This class assumes there will be three major screen sizes, and provides layout - sizes, spacing, and styles in order to resize the passcode view for each one. - - The three screen styles it supports are: - - * Small Screens - iPhone 5/ or iPad 9.7" in 1/4 split screen mode - * Medium Screens - iPhone 6/ or iPad 12.9" in 1/4 split screen mode - * Large Screens - iPhone 6 Plus and all iPads when not in split screen mode. - - */ -@interface TOPasscodeViewContentLayout : NSObject - -/* The width of the PIN view in which this layout object is sizing the content to fit. */ -@property (nonatomic, assign) CGFloat viewWidth; - -/* Extra padding at the bottom in order to shift the content slightly up */ -@property (nonatomic, assign) CGFloat bottomPadding; - -/* The title view at the very top */ -@property (nonatomic, assign) CGFloat titleViewBottomSpacing; // Space from the bottom of the title view to the title label - -/* The Title Label Explaining the Passcode View */ -@property (nonatomic, assign) CGFloat titleLabelBottomSpacing; // Space from the title label to the input view -@property (nonatomic, assign) CGFloat subtitleLabelBottomSpacing; // Space from the subtitle label to the input view - -@property (nonatomic, strong) UIFont *titleLabelFont; // The font of the title label -@property (nonatomic, strong) UIFont *subtitleLabelFont; // The font of the subtitle label - -/* Title Label properties when the view is laid out horizontally */ -@property (nonatomic, assign) CGFloat titleHorizontalLayoutWidth; // When laid out horizontally, the width of the title view -@property (nonatomic, assign) CGFloat titleHorizontalLayoutSpacing; // The amount of spacing between the title label and the passcode keypad -@property (nonatomic, assign) CGFloat titleViewHorizontalBottomSpacing; // Space from the bottom of the title view when iPhone is horizontal -@property (nonatomic, assign) CGFloat titleLabelHorizontalBottomSpacing; // Spacing from the title label to input view in horizontal mode -@property (nonatomic, assign) CGFloat subtitleLabelHorizontalBottomSpacing; // Spacing from the subtitle label to input view in horizontal mode - -/* Circle Row Configuration */ -@property (nonatomic, assign) CGFloat circleRowDiameter; // The diameter of each circle representing a PIN number -@property (nonatomic, assign) CGFloat circleRowSpacing; // The spacing between each circle -@property (nonatomic, assign) CGFloat circleRowBottomSpacing; // Space between the view used to indicate input - -/* Text Field Configuration */ -@property (nonatomic, assign) CGFloat textFieldBorderThickness; // The thickness of the border stroke -@property (nonatomic, assign) CGFloat textFieldBorderRadius; // The corner radius of the border -@property (nonatomic, assign) CGFloat textFieldCircleDiameter; // The size of the circles in the passcode field -@property (nonatomic, assign) CGFloat textFieldCircleSpacing; // The amount of spacing between each circle -@property (nonatomic, assign) CGSize textFieldBorderPadding; // The amount of padding between the circles and the border -@property (nonatomic, assign) NSInteger textFieldNumericCharacterLength; // The amount of circles to have in this field when set to numeric -@property (nonatomic, assign) NSInteger textFieldAlphanumericCharacterLength; // The amount of circles to have in this field when set to alphanumeric -@property (nonatomic, assign) CGFloat submitButtonFontSize; // The font size of the 'OK' button -@property (nonatomic, assign) CGFloat submitButtonSpacing; // The spacing of the 'OK' button from the input - -/* Circle Button Shape and Layout */ -@property (nonatomic, assign) CGFloat circleButtonDiameter; // The size of each PIN button -@property (nonatomic, assign) CGSize circleButtonSpacing; // The vertical/horizontal spacing between buttons -@property (nonatomic, assign) CGFloat circleButtonStrokeWidth; // The thickness of the border line - -/* Circle Button Label */ -@property (nonatomic, strong) UIFont *circleButtonTitleLabelFont; // The font used for the '1' number labels -@property (nonatomic, strong) UIFont *circleButtonLetteringLabelFont; // The font used for the 'ABC' labels -@property (nonatomic, assign) CGFloat circleButtonLabelSpacing; // The vertical spacing between the number and lettering labels -@property (nonatomic, assign) CGFloat circleButtonLetteringSpacing; // The spacing between the 'ABC' characters - -/* Default layout configurations for the various sizes */ -+ (TOPasscodeViewContentLayout *)defaultScreenContentLayout; /* Default layout values. Designed for iPhone 6 Plus and above. */ -+ (TOPasscodeViewContentLayout *)mediumScreenContentLayout; /* For medium screen sizes, like iPhone 6, or 1/4 view on iPad Pro. */ -+ (TOPasscodeViewContentLayout *)smallScreenContentLayout; /* For the smallest screens, like iPhone SE, and 1/4 on standard size iPads/ */ - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m deleted file mode 100644 index 5e8029133c..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewContentLayout.m +++ /dev/null @@ -1,205 +0,0 @@ -// -// TOPasscodeViewContentLayout.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeViewContentLayout.h" - -@implementation TOPasscodeViewContentLayout - -+ (TOPasscodeViewContentLayout *)defaultScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 414.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 25.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 34.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelBottomSpacing = 34.0f; - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 22.0f]; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 250.0f; - contentLayout.titleHorizontalLayoutSpacing = 35.0f; - contentLayout.titleViewHorizontalBottomSpacing = 20.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 20.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 15.5f; - contentLayout.circleRowSpacing = 30.0f; - contentLayout.circleRowBottomSpacing = 35.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 17.0f]; - contentLayout.subtitleLabelBottomSpacing = 40.0f; - contentLayout.subtitleLabelHorizontalBottomSpacing = 40.0f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 10.0f; - contentLayout.textFieldCircleSpacing = 6.0f; - contentLayout.textFieldBorderPadding = (CGSize){10, 10}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - contentLayout.submitButtonFontSize = 17.0f; - contentLayout.submitButtonSpacing = 4.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 90.0f; - contentLayout.circleButtonSpacing = (CGSize){25.0f, 16.0f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 6.0f; - contentLayout.circleButtonLetteringSpacing = 3.0f; - - return contentLayout; -} - -+ (TOPasscodeViewContentLayout *)mediumScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 375.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 17.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 20.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 19.0f]; - contentLayout.titleLabelBottomSpacing = 23.0f; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 185.0f; - contentLayout.titleHorizontalLayoutSpacing = 16.0f; - contentLayout.titleViewHorizontalBottomSpacing = 18.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 13.5f; - contentLayout.circleRowSpacing = 26.0f; - contentLayout.circleRowBottomSpacing = 21.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 14.0f]; - contentLayout.subtitleLabelHorizontalBottomSpacing = 20.0f; - contentLayout.subtitleLabelBottomSpacing = 20.0f; - - /* Submit Button */ - contentLayout.submitButtonFontSize = 16.0f; - contentLayout.submitButtonSpacing = 4.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 80.0f; - contentLayout.circleButtonSpacing = (CGSize){28.0f, 15.0f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 9.0f; - contentLayout.textFieldCircleSpacing = 5.0f; - contentLayout.textFieldBorderPadding = (CGSize){10, 10}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:36.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:8.5f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 5.0f; - contentLayout.circleButtonLetteringSpacing = 2.5f; - - return contentLayout; -} - -+ (TOPasscodeViewContentLayout *)smallScreenContentLayout -{ - TOPasscodeViewContentLayout *contentLayout = [[TOPasscodeViewContentLayout alloc] init]; - - /* Width of the PIN View */ - contentLayout.viewWidth = 320.0f; - - /* Bottom Padding */ - contentLayout.bottomPadding = 12.0f; - - /* Title View Constraints */ - contentLayout.titleViewBottomSpacing = 15.0f; - - /* The Title Label Explaining the PIN View */ - contentLayout.titleLabelFont = [UIFont systemFontOfSize: 16.0f]; - contentLayout.titleLabelBottomSpacing = 19.0f; - - /* Horizontal title constraints */ - contentLayout.titleHorizontalLayoutWidth = 185.0f; - contentLayout.titleHorizontalLayoutSpacing = 5.0f; - contentLayout.titleViewHorizontalBottomSpacing = 18.0f; - contentLayout.titleLabelHorizontalBottomSpacing = 18.0f; - - /* Circle Row Configuration */ - contentLayout.circleRowDiameter = 12.5f; - contentLayout.circleRowSpacing = 22.0f; - contentLayout.circleRowBottomSpacing = 19.0f; - - /* The Subtitle Label */ - contentLayout.subtitleLabelFont = [UIFont systemFontOfSize: 12.0f]; - contentLayout.subtitleLabelHorizontalBottomSpacing = 22.0f; - contentLayout.subtitleLabelBottomSpacing = 19.0f; - - /* Text Field Input Configuration */ - contentLayout.textFieldBorderThickness = 1.5f; - contentLayout.textFieldBorderRadius = 5.0f; - contentLayout.textFieldCircleDiameter = 8.0f; - contentLayout.textFieldCircleSpacing = 4.0f; - contentLayout.textFieldBorderPadding = (CGSize){8, 8}; - contentLayout.textFieldNumericCharacterLength = 10; - contentLayout.textFieldAlphanumericCharacterLength = 15; - - /* Submit Button */ - contentLayout.submitButtonFontSize = 15.0f; - contentLayout.submitButtonSpacing = 3.0f; - - /* Circle Button Shape and Layout */ - contentLayout.circleButtonDiameter = 70.0f; - contentLayout.circleButtonSpacing = (CGSize){20.0f, 8.5f}; - contentLayout.circleButtonStrokeWidth = 1.5f; - - /* Circle Button Label */ - contentLayout.circleButtonTitleLabelFont = [UIFont systemFontOfSize:35.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLetteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - contentLayout.circleButtonLabelSpacing = 4.5f; - contentLayout.circleButtonLetteringSpacing = 2.0f; - - return contentLayout; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h deleted file mode 100644 index 23147a4481..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.h +++ /dev/null @@ -1,60 +0,0 @@ -// -// TOPasscodeViewControllerAnimatedTransitioning.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import - -@class TOPasscodeViewController; - -NS_ASSUME_NONNULL_BEGIN - -/** - An class conforming to `UIViewControllerAnimatedTransitioning` that handles the custom animation - that plays when the passcode view controller is presented on the user's screen. - */ -@interface TOPasscodeViewControllerAnimatedTransitioning : NSObject - -/** The parent passcode view controller that this object will be controlling */ -@property (nonatomic, weak, readonly) TOPasscodeViewController *passcodeViewController; - -/** Whether the controller is being presented or dismissed. The animation is played in reverse when dismissing. */ -@property (nonatomic, assign) BOOL dismissing; - -/** If the correct passcode was successfully entered, this property can be set to YES. When the view controller - is dismissing, the keypad view will also play a zooming out animation to give added context to the dismissal. */ -@property (nonatomic, assign) BOOL success; - -/** - Creates a new instanc of `TOPasscodeViewControllerAnimatedTransitioning` that will control the provided passcode - view controller. - - @param passcodeViewController The passcode view controller in which this object will coordinate the animation upon. - @param dismissing Whether the animation is played to present the view controller, or dismiss it. - @param success Whether the object needs to play an additional zooming animation denoting the passcode was successfully entered. - */ -- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController - dismissing:(BOOL)dismissing - success:(BOOL)success; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m deleted file mode 100644 index fe5f4c2e4d..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOPasscodeViewControllerAnimatedTransitioning.m +++ /dev/null @@ -1,120 +0,0 @@ -// -// TOPasscodeViewControllerAnimatedTransitioning.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeViewControllerAnimatedTransitioning.h" -#import "TOPasscodeViewController.h" -#import "TOPasscodeView.h" - -@interface TOPasscodeViewControllerAnimatedTransitioning () -@property (nonatomic, weak) TOPasscodeViewController *passcodeViewController; -@end - -@implementation TOPasscodeViewControllerAnimatedTransitioning - -- (instancetype)initWithPasscodeViewController:(TOPasscodeViewController *)passcodeViewController dismissing:(BOOL)dismissing success:(BOOL)success -{ - if (self = [super init]) { - _passcodeViewController = passcodeViewController; - _dismissing = dismissing; - _success = success; - } - - return self; -} - -- (NSTimeInterval)transitionDuration:(nullable id )transitionContext -{ - return 0.35f; -} - -- (void)animateTransition:(id )transitionContext -{ - BOOL isPhone = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone; - UIView *containerView = transitionContext.containerView; - UIVisualEffectView *backgroundEffectView = self.passcodeViewController.backgroundEffectView; - UIView *backgroundView = self.passcodeViewController.backgroundView; - UIVisualEffect *backgroundEffect = backgroundEffectView.effect; - TOPasscodeView *passcodeView = self.passcodeViewController.passcodeView; - - // Set the initial properties when presenting - if (!self.dismissing) { - backgroundEffectView.effect = nil; - backgroundView.alpha = 0.0f; - - self.passcodeViewController.view.frame = containerView.bounds; - [containerView addSubview:self.passcodeViewController.view]; - } - else { - UIViewController *baseController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; - if (baseController.view.superview == nil) { - [containerView insertSubview:baseController.view atIndex:0]; - } - } - - CGFloat alpha = self.dismissing ? 1.0f : 0.0f; - passcodeView.contentAlpha = alpha; - - // Animate the accessory views - if (isPhone) { - self.passcodeViewController.leftAccessoryButton.alpha = alpha; - self.passcodeViewController.rightAccessoryButton.alpha = alpha; - self.passcodeViewController.cancelButton.alpha = alpha; - self.passcodeViewController.biometricButton.alpha = alpha; - } - - id animationBlock = ^{ - backgroundEffectView.effect = self.dismissing ? nil : backgroundEffect; - backgroundView.alpha = self.dismissing ? 0.0f : 1.0f; - - CGFloat toAlpha = self.dismissing ? 0.0f : 1.0f; - passcodeView.contentAlpha = toAlpha; - if (isPhone) { - self.passcodeViewController.leftAccessoryButton.alpha = toAlpha; - self.passcodeViewController.rightAccessoryButton.alpha = toAlpha; - self.passcodeViewController.cancelButton.alpha = toAlpha; - self.passcodeViewController.biometricButton.alpha = toAlpha; - } - }; - - id completedBlock = ^(BOOL completed) { - backgroundEffectView.effect = backgroundEffect; - [transitionContext completeTransition:completed]; - }; - - // If we're animating out from a successful passcode, play a zooming out animation - // to give some more context - if (self.success && self.dismissing) { - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"]; - animation.duration = [self transitionDuration:transitionContext]; - animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.9f, 0.9f, 1)]; - animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - [passcodeView.layer addAnimation:animation forKey:@"transform"]; - } - - [UIView animateWithDuration:[self transitionDuration:transitionContext] - delay:0.0f - options:UIViewAnimationOptionAllowUserInteraction - animations:animationBlock - completion:completedBlock]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h deleted file mode 100644 index 51d2828aae..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// TOSettingsKeypadImage.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -NS_ASSUME_NONNULL_BEGIN - -/** - A subclass of `UIImage` that procedurally generates images for `TOPasscodeSettingsKeypadView`. - This includes background images for each keypad button, and a delete icon for the bottom right corner - of the keypad. - */ -@interface TOSettingsKeypadImage : UIImage - -/** - Generates and returns an image of button background with a raised border in a pseudo-skeuomorphic style. - - @param radius The rounded radius of the button image's corners - @param foregroundColor The fill color of the primary section of the button - @param edgeColor The color of the raised border edge along the bottom. - @param thickness The size of the border running along the bottom - */ -+ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius - foregroundColor:(UIColor *)foregroundColor - edgeColor:(UIColor *)edgeColor - edgeThickness:(CGFloat)thickness; - -/** -Generates and returns a tintable delete icon. - */ -+ (UIImage *)deleteIcon; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m b/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m deleted file mode 100644 index e361081bd3..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Models/TOSettingsKeypadImage.m +++ /dev/null @@ -1,186 +0,0 @@ -// -// TOSettingsKeypadImage.m -// TOPasscodeViewControllerExample -// -// Created by Tim Oliver on 6/20/17. -// Copyright © 2017 Timothy Oliver. All rights reserved. -// - -#import "TOSettingsKeypadImage.h" - -#define TOP_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define TOP_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + Y * limitedRadius) -#define BOTTOM_RIGHT(X, Y) CGPointMake(rect.origin.x + rect.size.width - X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) -#define BOTTOM_LEFT(X, Y) CGPointMake(rect.origin.x + X * limitedRadius, rect.origin.y + rect.size.height - Y * limitedRadius) - -@implementation TOSettingsKeypadImage - -+ (UIImage *)buttonImageWithCornerRadius:(CGFloat)radius - foregroundColor:(UIColor *)foregroundColor - edgeColor:(UIColor *)edgeColor - edgeThickness:(CGFloat)thickness -{ - CGFloat width = (radius * 2.0f) + 1.0f; - CGFloat height = width + thickness; - - CGRect frame = (CGRect){CGPointZero, {width, height}}; - - UIImage *image = nil; - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - CGContextRef context = UIGraphicsGetCurrentContext(); - - NSShadow* shadow = [[NSShadow alloc] init]; - shadow.shadowColor = edgeColor; - shadow.shadowOffset = CGSizeMake(0, thickness); - shadow.shadowBlurRadius = 0; - - CGRect buttonFrame = frame; - buttonFrame.size.height -= thickness; - - CGContextSaveGState(context); - { - CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, [shadow.shadowColor CGColor]); - UIBezierPath *buttonPath = [[self class] bezierPathWithContinuousRoundedRect:buttonFrame cornerRadius:radius];//bezierPathWithRoundedRect:buttonFrame cornerRadius:radius]; - [foregroundColor setFill]; - [buttonPath fill]; - } - CGContextRestoreGState(context); - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - UIEdgeInsets insets = UIEdgeInsetsMake(radius, radius, radius + thickness, radius); - image = [image resizableImageWithCapInsets:insets]; - - return image; -} - -+ (UIImage *)deleteIcon -{ - UIImage *image = nil; - - CGRect frame = CGRectMake(0, 0, 40.0f, 21.0f); - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - // DeleteIcon - { - // Border Drawing - UIBezierPath* borderPath = [UIBezierPath bezierPath]; - [borderPath moveToPoint: CGPointMake(25.73, 1.5)]; - [borderPath addLineToPoint: CGPointMake(25.9, 1.53)]; - [borderPath addCurveToPoint: CGPointMake(28.34, 3.46) controlPoint1: CGPointMake(27.03, 1.86) controlPoint2: CGPointMake(27.93, 2.56)]; - [borderPath addCurveToPoint: CGPointMake(28.67, 6.56) controlPoint1: CGPointMake(28.67, 4.28) controlPoint2: CGPointMake(28.67, 5.04)]; - [borderPath addLineToPoint: CGPointMake(28.64, 14.23)]; - [borderPath addCurveToPoint: CGPointMake(28.35, 17.05) controlPoint1: CGPointMake(28.64, 15.76) controlPoint2: CGPointMake(28.64, 16.37)]; - [borderPath addLineToPoint: CGPointMake(28.31, 17.19)]; - [borderPath addCurveToPoint: CGPointMake(25.86, 19.11) controlPoint1: CGPointMake(27.89, 18.08) controlPoint2: CGPointMake(27, 18.79)]; - [borderPath addCurveToPoint: CGPointMake(21.4, 19.37) controlPoint1: CGPointMake(24.82, 19.37) controlPoint2: CGPointMake(23.34, 19.37)]; - [borderPath addLineToPoint: CGPointMake(11.51, 19.37)]; - [borderPath addCurveToPoint: CGPointMake(9.9, 19.07) controlPoint1: CGPointMake(11.51, 19.37) controlPoint2: CGPointMake(10.41, 19.3)]; - [borderPath addCurveToPoint: CGPointMake(7.38, 17.06) controlPoint1: CGPointMake(9.09, 18.68) controlPoint2: CGPointMake(8.52, 18.14)]; - [borderPath addLineToPoint: CGPointMake(3.92, 13.81)]; - [borderPath addCurveToPoint: CGPointMake(1.87, 11.55) controlPoint1: CGPointMake(2.78, 12.73) controlPoint2: CGPointMake(2.21, 12.19)]; - [borderPath addLineToPoint: CGPointMake(1.79, 11.43)]; - [borderPath addCurveToPoint: CGPointMake(1.82, 9.06) controlPoint1: CGPointMake(1.36, 10.57) controlPoint2: CGPointMake(1.4, 9.92)]; - [borderPath addCurveToPoint: CGPointMake(3.96, 6.68) controlPoint1: CGPointMake(2.25, 8.29) controlPoint2: CGPointMake(2.82, 7.76)]; - [borderPath addLineToPoint: CGPointMake(7.21, 3.61)]; - [borderPath addCurveToPoint: CGPointMake(9.61, 1.67) controlPoint1: CGPointMake(8.35, 2.54) controlPoint2: CGPointMake(8.92, 2)]; - [borderPath addLineToPoint: CGPointMake(9.73, 1.6)]; - [borderPath addCurveToPoint: CGPointMake(11.41, 1.31) controlPoint1: CGPointMake(10.26, 1.37) controlPoint2: CGPointMake(10.84, 1.27)]; - [borderPath addLineToPoint: CGPointMake(21.44, 1.27)]; - [borderPath addCurveToPoint: CGPointMake(25.73, 1.5) controlPoint1: CGPointMake(23.38, 1.27) controlPoint2: CGPointMake(24.85, 1.27)]; - [borderPath closePath]; - [UIColor.blackColor setStroke]; - borderPath.lineWidth = 2.5; - [borderPath stroke]; - - - //// Cross Drawing - UIBezierPath* crossPath = [UIBezierPath bezierPath]; - [crossPath moveToPoint: CGPointMake(15.22, 5.9)]; - [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(15.27, 5.95) controlPoint2: CGPointMake(15.21, 5.88)]; - [crossPath addLineToPoint: CGPointMake(15.22, 5.9)]; - [crossPath closePath]; - [crossPath moveToPoint: CGPointMake(16.18, 10.28)]; - [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(16.22, 10.29) controlPoint2: CGPointMake(16.2, 10.28)]; - [crossPath addLineToPoint: CGPointMake(16.18, 10.28)]; - [crossPath closePath]; - [crossPath moveToPoint: CGPointMake(14.52, 5.35)]; - [crossPath addCurveToPoint: CGPointMake(15.21, 5.88) controlPoint1: CGPointMake(14.75, 5.46) controlPoint2: CGPointMake(14.93, 5.62)]; - [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.26, 5.94) controlPoint2: CGPointMake(15.32, 5.99)]; - [crossPath addCurveToPoint: CGPointMake(15.43, 6.09) controlPoint1: CGPointMake(15.42, 6.09) controlPoint2: CGPointMake(15.43, 6.09)]; - [crossPath addCurveToPoint: CGPointMake(15.38, 6.05) controlPoint1: CGPointMake(15.21, 5.88) controlPoint2: CGPointMake(15.27, 5.95)]; - [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(15.94, 6.59) controlPoint2: CGPointMake(17.66, 8.25)]; - [crossPath addCurveToPoint: CGPointMake(17.97, 8.55) controlPoint1: CGPointMake(17.91, 8.61) controlPoint2: CGPointMake(17.94, 8.58)]; - [crossPath addCurveToPoint: CGPointMake(21.36, 5.39) controlPoint1: CGPointMake(20.95, 5.68) controlPoint2: CGPointMake(21.14, 5.5)]; - [crossPath addCurveToPoint: CGPointMake(22.67, 5.58) controlPoint1: CGPointMake(21.83, 5.17) controlPoint2: CGPointMake(22.34, 5.26)]; - [crossPath addCurveToPoint: CGPointMake(22.98, 6.89) controlPoint1: CGPointMake(23.09, 5.99) controlPoint2: CGPointMake(23.18, 6.47)]; - [crossPath addCurveToPoint: CGPointMake(22.28, 7.68) controlPoint1: CGPointMake(22.84, 7.14) controlPoint2: CGPointMake(22.65, 7.32)]; - [crossPath addCurveToPoint: CGPointMake(19.68, 10.19) controlPoint1: CGPointMake(22.28, 7.68) controlPoint2: CGPointMake(20.88, 9.03)]; - [crossPath addCurveToPoint: CGPointMake(22.97, 13.47) controlPoint1: CGPointMake(22.66, 13.06) controlPoint2: CGPointMake(22.85, 13.25)]; - [crossPath addCurveToPoint: CGPointMake(22.76, 14.79) controlPoint1: CGPointMake(23.21, 13.95) controlPoint2: CGPointMake(23.11, 14.46)]; - [crossPath addCurveToPoint: CGPointMake(21.35, 15.1) controlPoint1: CGPointMake(22.33, 15.22) controlPoint2: CGPointMake(21.8, 15.31)]; - [crossPath addCurveToPoint: CGPointMake(20.48, 14.4) controlPoint1: CGPointMake(21.07, 14.97) controlPoint2: CGPointMake(20.87, 14.78)]; - [crossPath addCurveToPoint: CGPointMake(17.89, 11.91) controlPoint1: CGPointMake(20.48, 14.4) controlPoint2: CGPointMake(19.08, 13.05)]; - [crossPath addCurveToPoint: CGPointMake(14.5, 15.06) controlPoint1: CGPointMake(14.91, 14.78) controlPoint2: CGPointMake(14.73, 14.95)]; - [crossPath addCurveToPoint: CGPointMake(13.2, 14.87) controlPoint1: CGPointMake(14.04, 15.28) controlPoint2: CGPointMake(13.53, 15.19)]; - [crossPath addCurveToPoint: CGPointMake(12.89, 13.57) controlPoint1: CGPointMake(12.78, 14.47) controlPoint2: CGPointMake(12.69, 13.98)]; - [crossPath addCurveToPoint: CGPointMake(13.42, 12.93) controlPoint1: CGPointMake(13, 13.35) controlPoint2: CGPointMake(13.15, 13.19)]; - [crossPath addCurveToPoint: CGPointMake(13.59, 12.77) controlPoint1: CGPointMake(13.47, 12.88) controlPoint2: CGPointMake(13.53, 12.83)]; - [crossPath addCurveToPoint: CGPointMake(16.19, 10.26) controlPoint1: CGPointMake(14.12, 12.25) controlPoint2: CGPointMake(15.78, 10.66)]; - [crossPath addCurveToPoint: CGPointMake(12.89, 6.98) controlPoint1: CGPointMake(13.21, 7.39) controlPoint2: CGPointMake(13.01, 7.2)]; - [crossPath addCurveToPoint: CGPointMake(12.77, 6.63) controlPoint1: CGPointMake(12.82, 6.84) controlPoint2: CGPointMake(12.79, 6.73)]; - [crossPath addCurveToPoint: CGPointMake(13.1, 5.66) controlPoint1: CGPointMake(12.72, 6.28) controlPoint2: CGPointMake(12.83, 5.92)]; - [crossPath addCurveToPoint: CGPointMake(14.52, 5.35) controlPoint1: CGPointMake(13.54, 5.24) controlPoint2: CGPointMake(14.07, 5.15)]; - [crossPath closePath]; - [UIColor.blackColor setFill]; - [crossPath fill]; - } - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -/** - Creates a bezier path with the iOS 7 squircle shape. - - A HUGE thanks to the folks at PaintCode for open-sourcing this - https://www.paintcodeapp.com/news/code-for-ios-7-rounded-rectangles - */ -+ (UIBezierPath *)bezierPathWithContinuousRoundedRect:(CGRect)rect cornerRadius:(CGFloat)radius -{ - UIBezierPath* path = UIBezierPath.bezierPath; - CGFloat limit = MIN(rect.size.width, rect.size.height) / 2 / 1.52866483; - CGFloat limitedRadius = MIN(radius, limit); - - [path moveToPoint: TOP_LEFT(1.52866483, 0.00000000)]; - [path addLineToPoint: TOP_RIGHT(1.52866471, 0.00000000)]; - [path addCurveToPoint: TOP_RIGHT(0.66993427, 0.06549600) controlPoint1: TOP_RIGHT(1.08849323, 0.00000000) controlPoint2: TOP_RIGHT(0.86840689, 0.00000000)]; - [path addLineToPoint: TOP_RIGHT(0.63149399, 0.07491100)]; - [path addCurveToPoint: TOP_RIGHT(0.07491176, 0.63149399) controlPoint1: TOP_RIGHT(0.37282392, 0.16905899) controlPoint2: TOP_RIGHT(0.16906013, 0.37282401)]; - [path addCurveToPoint: TOP_RIGHT(0.00000000, 1.52866483) controlPoint1: TOP_RIGHT(0.00000000, 0.86840701) controlPoint2: TOP_RIGHT(0.00000000, 1.08849299)]; - [path addLineToPoint: BOTTOM_RIGHT(0.00000000, 1.52866471)]; - [path addCurveToPoint: BOTTOM_RIGHT(0.06549569, 0.66993493) controlPoint1: BOTTOM_RIGHT(0.00000000, 1.08849323) controlPoint2: BOTTOM_RIGHT(0.00000000, 0.86840689)]; - [path addLineToPoint: BOTTOM_RIGHT(0.07491111, 0.63149399)]; - [path addCurveToPoint: BOTTOM_RIGHT(0.63149399, 0.07491111) controlPoint1: BOTTOM_RIGHT(0.16905883, 0.37282392) controlPoint2: BOTTOM_RIGHT(0.37282392, 0.16905883)]; - [path addCurveToPoint: BOTTOM_RIGHT(1.52866471, 0.00000000) controlPoint1: BOTTOM_RIGHT(0.86840689, 0.00000000) controlPoint2: BOTTOM_RIGHT(1.08849323, 0.00000000)]; - [path addLineToPoint: BOTTOM_LEFT(1.52866483, 0.00000000)]; - [path addCurveToPoint: BOTTOM_LEFT(0.66993397, 0.06549569) controlPoint1: BOTTOM_LEFT(1.08849299, 0.00000000) controlPoint2: BOTTOM_LEFT(0.86840701, 0.00000000)]; - [path addLineToPoint: BOTTOM_LEFT(0.63149399, 0.07491111)]; - [path addCurveToPoint: BOTTOM_LEFT(0.07491100, 0.63149399) controlPoint1: BOTTOM_LEFT(0.37282401, 0.16905883) controlPoint2: BOTTOM_LEFT(0.16906001, 0.37282392)]; - [path addCurveToPoint: BOTTOM_LEFT(0.00000000, 1.52866471) controlPoint1: BOTTOM_LEFT(0.00000000, 0.86840689) controlPoint2: BOTTOM_LEFT(0.00000000, 1.08849323)]; - [path addLineToPoint: TOP_LEFT(0.00000000, 1.52866483)]; - [path addCurveToPoint: TOP_LEFT(0.06549600, 0.66993397) controlPoint1: TOP_LEFT(0.00000000, 1.08849299) controlPoint2: TOP_LEFT(0.00000000, 0.86840701)]; - [path addLineToPoint: TOP_LEFT(0.07491100, 0.63149399)]; - [path addCurveToPoint: TOP_LEFT(0.63149399, 0.07491100) controlPoint1: TOP_LEFT(0.16906001, 0.37282401) controlPoint2: TOP_LEFT(0.37282401, 0.16906001)]; - [path addCurveToPoint: TOP_LEFT(1.52866483, 0.00000000) controlPoint1: TOP_LEFT(0.86840701, 0.00000000) controlPoint2: TOP_LEFT(1.08849299, 0.00000000)]; - [path closePath]; - return path; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h b/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h deleted file mode 100644 index 3447370059..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Supporting/TOPasscodeViewControllerConstants.h +++ /dev/null @@ -1,71 +0,0 @@ -// -// TOPasscodeViewControllerConstants.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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. - -/* The visual style of the asscode view controller */ -typedef NS_ENUM(NSInteger, TOPasscodeViewStyle) { - TOPasscodeViewStyleTranslucentDark, - TOPasscodeViewStyleTranslucentLight, - TOPasscodeViewStyleOpaqueDark, - TOPasscodeViewStyleOpaqueLight -}; - -/* The visual style of the passcode settings view controller. */ -typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewStyle) { - TOPasscodeSettingsViewStyleLight, - TOPasscodeSettingsViewStyleDark -}; - -/* Depending on the amount of horizontal space, the sizing of the elements */ -typedef NS_ENUM(NSInteger, TOPasscodeViewContentSize) { - TOPasscodeViewContentSizeDefault = 414, // Default, 414 points and above (6 Plus, all remaining iPad sizes) - TOPasscodeViewContentSizeMedium = 375, // Greater or equal to 375 points: iPhone 6 / iPad Pro 1/4 split mode - TOPasscodeViewContentSizeSmall = 320 // Greater or equal to 320 points: iPhone SE / iPad 1/4 split mode -}; - -/* The types of passcodes that may be used. */ -typedef NS_ENUM(NSInteger, TOPasscodeType) { - TOPasscodeTypeFourDigits, // 4 Numbers - TOPasscodeTypeSixDigits, // 6 Numbers - TOPasscodeTypeCustomNumeric, // Any length of numbers - TOPasscodeTypeCustomAlphanumeric // Any length of characters -}; - -/* The type of biometrics this controller can handle */ -typedef NS_ENUM(NSInteger, TOPasscodeBiometryType) { - TOPasscodeBiometryTypeTouchID, - TOPasscodeBiometryTypeFaceID -}; - -static inline BOOL TOPasscodeViewStyleIsTranslucent(TOPasscodeViewStyle style) { - return style <= TOPasscodeViewStyleTranslucentLight; -} - -static inline BOOL TOPasscodeViewStyleIsDark(TOPasscodeViewStyle style) { - return style < TOPasscodeViewStyleTranslucentLight || style == TOPasscodeViewStyleOpaqueDark; -} - -static inline NSString *TOPasscodeBiometryTitleForType(TOPasscodeBiometryType type) { - switch (type) { - case TOPasscodeBiometryTypeFaceID: return NSLocalizedString(@"Face ID", @""); - default: return NSLocalizedString(@"Touch ID", @""); - } -} diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h deleted file mode 100644 index 656e617dc7..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.h +++ /dev/null @@ -1,108 +0,0 @@ -// -// TOPasscodeSettingsViewController.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import "TOPasscodeViewControllerConstants.h" - -@class TOPasscodeSettingsViewController; - -typedef NS_ENUM(NSInteger, TOPasscodeSettingsViewState) { - TOPasscodeSettingsViewStateEnterCurrentPasscode, - TOPasscodeSettingsViewStateEnterNewPasscode, - TOPasscodeSettingsViewStateConfirmNewPasscode -}; - -NS_ASSUME_NONNULL_BEGIN - -/** - A delegate object in charge of validating and recording the passcodes entered by the user. - */ -@protocol TOPasscodeSettingsViewControllerDelegate - -@optional - -/** Called when the user was prompted to input their current passcode. - Return YES if passcode was right and NO otherwise. - - Returning NO will cause a warning label to appear - */ -- (BOOL)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController - didAttemptCurrentPasscode:(NSString *)passcode; - -/** Called when the user has successfully set a new passcode. At this point, you should save over - the old passcode with the new one. */ -- (void)passcodeSettingsViewController:(TOPasscodeSettingsViewController *)passcodeSettingsViewController - didChangeToNewPasscode:(NSString *)passcode ofType:(TOPasscodeType)type; - -@end - -// ---------------------------------------------------------------------- - -/** - A standard system-styled view controller that users can use to change the passcode - that they will need to enter for the main passcode view controller. - - This controller allows requiring the user to enter their previous passcode in first, - and has passcode validation by requiring them to enter the new passcode twice. - */ - -@interface TOPasscodeSettingsViewController : UIViewController - -/** Delegate event for controlling and responding to the behavior of this controller */ -@property (nonatomic, weak, nullable) id delegate; - -/** The current state of the controller (confirming old passcode or creating a new one) */ -@property (nonatomic, assign) TOPasscodeSettingsViewState state; - -/** The input type of the passcode */ -@property (nonatomic, assign) TOPasscodeType passcodeType; - -/** The number of incorrect passcode attempts the user has made. Use this property to decide when to disable input. */ -@property (nonatomic, assign) NSInteger failedPasscodeAttemptCount; - -/** Before setting a new passcode, show a UI to validate the existing passcode. (Default is NO) */ -@property (nonatomic, assign) BOOL requireCurrentPasscode; - -/** If set, the view controller will disable input until this date time has been reached */ -@property (nonatomic, strong, nullable) NSDate *disabledInputDate; - -/** Hide the button Options (Default is NO) */ -@property (nonatomic, assign) BOOL hideOptionsButton; - -/* - Create a new instance with the desird light or dark style - - @param style The visual style of the view controller - */ -- (instancetype)init; - -/* - Changes the passcode type and animates if required - - @param passcodeType Change the type of passcode to enter. - @param animated Play a crossfade animation. - */ -- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m deleted file mode 100644 index 707f2567dc..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeSettingsViewController.m +++ /dev/null @@ -1,630 +0,0 @@ -// -// TOPasscodeSettingsViewController.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeSettingsViewController.h" -#import "TOPasscodeInputField.h" -#import "TOPasscodeSettingsKeypadView.h" -#import "TOPasscodeSettingsWarningLabel.h" - -const CGFloat kTOPasscodeSettingsLabelInputSpacing = 15.0f; -const CGFloat kTOPasscodeSettingsOptionsButtonOffset = 15.0f; -const CGFloat kTOPasscodeKeypadMaxSizeRatio = 0.40f; -const CGFloat kTOPasscodeKeypadMinHeight = 185.0f; // was 165 -const CGFloat kTOPasscodeKeypadMaxHeight = 330.0f; - -@interface TOPasscodeSettingsViewController () - -@property (nonatomic, copy) NSString *potentialPasscode; - -/* Layout Calculations */ -@property (nonatomic, assign) CGFloat verticalMidPoint; -@property (nonatomic, assign) CGRect keyboardFrame; -@property (nonatomic, readonly) CGRect contentOverlapFrame; // Either the keypad or the system keyboard - -/* Views */ -@property (nonatomic, strong) UIView *containerView; -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *errorLabel; -@property (nonatomic, strong) UIButton *optionsButton; -@property (nonatomic, strong) TOPasscodeInputField *inputField; -@property (nonatomic, strong) TOPasscodeSettingsKeypadView *keypadView; -@property (nonatomic, strong) TOPasscodeSettingsWarningLabel *warningLabel; - -/* Bar Items */ -@property (nonatomic, strong) UIBarButtonItem *nextBarButtonItem; -@property (nonatomic, strong) UIBarButtonItem *doneBarButtonItem; - -/* Style */ -@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; - -@end - -@implementation TOPasscodeSettingsViewController - -#pragma mark - Object Creation - - -- (instancetype)init -{ - if (self = [self initWithNibName:nil bundle:nil]) { - [self setUp]; - } - - return self; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - if (@available(iOS 13.0, *)) { - if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { - self.style = TOPasscodeSettingsViewStyleDark; - } else { - self.style = TOPasscodeSettingsViewStyleLight; - } - } else { - self.style = TOPasscodeSettingsViewStyleLight; - } - - [self applyThemeForStyle:_style]; - - _failedPasscodeAttemptCount = 0; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -#pragma mark - View Set-up - - -- (void)viewDidLoad { - [super viewDidLoad]; - - __weak typeof(self) weakSelf = self; - - self.title = NSLocalizedString(@"Enter Passcode", @""); - - // Create container view - self.containerView = [[UIView alloc] initWithFrame:CGRectZero]; - self.containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin - | UIViewAutoresizingFlexibleBottomMargin; - [self.view addSubview:self.containerView]; - - // Create title label - self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.titleLabel.font = [UIFont systemFontOfSize:17.0f]; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.textColor = [UIColor blackColor]; - self.titleLabel.text = @"Enter your passcode"; - self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [self.titleLabel sizeToFit]; - [self.containerView addSubview:self.titleLabel]; - - // Create number view - self.inputField = [[TOPasscodeInputField alloc] init]; - self.inputField.tintColor = [UIColor blackColor]; - self.inputField.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { [weakSelf inputViewDidCompletePasscode:passcode]; }; - [self.inputField sizeToFit]; - [self.containerView addSubview:self.inputField]; - - // Create keypad view - self.keypadView = [[TOPasscodeSettingsKeypadView alloc] initWithFrame:CGRectZero]; - self.keypadView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin; - [self.view addSubview:self.keypadView]; - - // Create warning label view - self.warningLabel = [[TOPasscodeSettingsWarningLabel alloc] initWithFrame:CGRectZero]; - self.warningLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - self.warningLabel.hidden = YES; - [self.warningLabel sizeToFit]; - [self.containerView addSubview:self.warningLabel]; - - // Create error label view - self.errorLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.errorLabel.text = NSLocalizedString(@"Passcodes didn't match. Try again.", @""); - self.errorLabel.textAlignment = NSTextAlignmentCenter; - self.errorLabel.font = [UIFont systemFontOfSize:15.0f]; - self.errorLabel.numberOfLines = 0; - self.errorLabel.hidden = YES; - [self.errorLabel sizeToFit]; - [self.containerView addSubview:self.errorLabel]; - - // Create Options button - self.optionsButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.optionsButton setTitle:NSLocalizedString(@"Passcode Options", @"") forState:UIControlStateNormal]; - self.optionsButton.titleLabel.font = [UIFont systemFontOfSize:15.0f]; - [self.optionsButton sizeToFit]; - self.optionsButton.hidden = _hideOptionsButton; - [self.optionsButton addTarget:self action:@selector(optionsCodeButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self.view addSubview:self.optionsButton]; - - // Add callbacks for the keypad view - self.keypadView.numberButtonTappedHandler = ^(NSInteger number) { - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; - [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; - }; - - self.keypadView.deleteButtonTappedHandler = ^{ [weakSelf.inputField deletePasscodeCharactersOfCount:1 animated:NO]; }; - - // Set height of the container view (This will never change) - CGRect frame = self.containerView.frame; - frame.size.width = self.view.bounds.size.width; - frame.size.height = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) - + CGRectGetHeight(self.warningLabel.frame) + (kTOPasscodeSettingsLabelInputSpacing * 2.0f); - self.containerView.frame = CGRectIntegral(frame); - - //Work out the vertical offset of the container view assuming the warning label doesn't count - self.verticalMidPoint = CGRectGetHeight(self.titleLabel.frame) + CGRectGetHeight(self.inputField.frame) - + kTOPasscodeSettingsLabelInputSpacing; - self.verticalMidPoint *= 0.5f; - - // Bar button items - self.nextBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Next", @"") style:UIBarButtonItemStylePlain target:self action:@selector(nextButtonTapped:)]; - self.doneBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(doneButtonTapped:)]; - - // Apply light/dark mode - [self applyThemeForStyle:self.style]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - self.state = self.requireCurrentPasscode ? TOPasscodeSettingsViewStateEnterCurrentPasscode : TOPasscodeSettingsViewStateEnterNewPasscode; - [self updateContentForState:self.state type:self.passcodeType animated:NO]; -} - -#pragma mark - View Update - - -- (void)updateContentForState:(TOPasscodeSettingsViewState)state type:(TOPasscodeType)type animated:(BOOL)animated -{ - BOOL variableSizePasscode = (type >= TOPasscodeTypeCustomNumeric); - - // Update the visibility of the options button - if (_hideOptionsButton) { - self.optionsButton.hidden = YES; - } else { - self.optionsButton.hidden = !(state == TOPasscodeSettingsViewStateEnterNewPasscode); - } - - // Clear the input view - self.inputField.passcode = nil; - - // Disable the input view - self.inputField.enabled = NO; - - //Update the warning label - [self updateWarningLabelForState:state]; - - // Change the input view if needed - if (!variableSizePasscode) { - self.inputField.style = TOPasscodeInputFieldStyleFixed; - self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; - } - else { - self.inputField.style = TOPasscodeInputFieldStyleVariable; - } - - // Update text depending on state - switch (state) { - case TOPasscodeSettingsViewStateEnterCurrentPasscode: - self.titleLabel.text = NSLocalizedString(@"Enter your passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; - if (@available(iOS 9.0, *)) { - self.inputField.returnKeyType = UIReturnKeyContinue; - } - else { - self.inputField.returnKeyType = UIReturnKeyNext; - } - break; - case TOPasscodeSettingsViewStateEnterNewPasscode: - self.titleLabel.text = NSLocalizedString(@"Enter a new passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.nextBarButtonItem : nil; - if (@available(iOS 9.0, *)) { - self.inputField.returnKeyType = UIReturnKeyContinue; - } - else { - self.inputField.returnKeyType = UIReturnKeyNext; - } - break; - case TOPasscodeSettingsViewStateConfirmNewPasscode: - self.titleLabel.text = NSLocalizedString(@"Confirm new passcode", @""); - self.navigationItem.rightBarButtonItem = variableSizePasscode ? self.doneBarButtonItem : nil; - self.inputField.returnKeyType = UIReturnKeyDone; - break; - } - - CGRect frame = CGRectZero; - - // Reload the 'Done' button - [self.inputField reloadInputViews]; - - // Resize text label to fit new text - [self.titleLabel sizeToFit]; - frame = self.titleLabel.frame; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - // Resize passcode view - [self.inputField sizeToFit]; - frame = self.inputField.frame; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.inputField.frame = CGRectIntegral(frame); - - // If we're the alphanumeric type, present the keyboard - if (type == TOPasscodeTypeCustomAlphanumeric) { - self.inputField.enabled = YES; - [self.inputField becomeFirstResponder]; - } - else { - if (self.inputField.isFirstResponder) { - [self.inputField resignFirstResponder]; - } - } - - // If not animated, force a blanket re-layout - if (!animated) { - [self viewDidLayoutSubviews]; - return; - } - - // If animated, perform the animation - [UIView animateWithDuration:0.3f animations:^{ - [self viewDidLayoutSubviews]; - }]; -} - -- (void)updateWarningLabelForState:(TOPasscodeSettingsViewState)state -{ - BOOL confirmingPasscode = state == TOPasscodeSettingsViewStateEnterCurrentPasscode; - - // Update the warning label - self.warningLabel.hidden = !(confirmingPasscode && self.failedPasscodeAttemptCount > 0); - self.warningLabel.numberOfWarnings = self.failedPasscodeAttemptCount; - - CGRect frame = self.warningLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - frame.size.width) * 0.5f; - self.warningLabel.frame = frame; -} - -- (void)transitionToState:(TOPasscodeSettingsViewState)state animated:(BOOL)animated -{ - // Preserve the current view state - UIView *snapshot = nil; - - BOOL reverseDirection = state < self.state; - - // If animated, take a snapshot of the current container view - if (animated) { - snapshot = [self.containerView snapshotViewAfterScreenUpdates:NO]; - snapshot.frame = self.containerView.frame; - [self.view addSubview:snapshot]; - } - - self.errorLabel.hidden = YES; - - // Update the layout for the new state - self.state = state; - - // Cancel out now if we're not animating - if (!animated) { - return; - } - - // Place the live container off screen to the right - CGFloat multiplier = reverseDirection ? -1.0f : 1.0f; - self.containerView.frame = CGRectOffset(self.containerView.frame, self.view.frame.size.width * multiplier, 0.0f); - - // Update the options button alpha depending on transition state - self.optionsButton.hidden = _hideOptionsButton; - self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 0.0f : 1.0f; - - // Perform an animation where the snapshot slides off, and the new container slides in - id animationBlock = ^{ - snapshot.frame = CGRectOffset(snapshot.frame, -self.view.frame.size.width * multiplier, 0.0f); - self.containerView.frame = CGRectOffset(self.containerView.frame, -self.view.frame.size.width * multiplier, 0.0f); - self.optionsButton.alpha = (state == TOPasscodeSettingsViewStateEnterNewPasscode) ? 1.0f : 0.0f; - }; - - // Clean up by removing the snapshot view - id completionBlock = ^(BOOL complete) { - [snapshot removeFromSuperview]; - }; - - // Perform the animation - [UIView animateWithDuration:0.4f - delay:0.0f - usingSpringWithDamping:1.0f - initialSpringVelocity:0.7f - options:0 - animations:animationBlock - completion:completionBlock]; -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - - CGSize viewSize = self.view.bounds.size; - - // Layout the keypad view - CGRect frame = self.keypadView.frame; - frame.size.height = viewSize.height * kTOPasscodeKeypadMaxSizeRatio; - frame.size.height = MAX(frame.size.height, kTOPasscodeKeypadMinHeight); - frame.size.height = MIN(frame.size.height, kTOPasscodeKeypadMaxHeight); - frame.size.width = viewSize.width; - frame.origin.y = viewSize.height; - if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - frame.origin.y -= frame.size.height; - } - - self.keypadView.frame = CGRectIntegral(frame); - - BOOL horizontalLayout = frame.size.height < kTOPasscodeKeypadMinHeight + FLT_EPSILON; - BOOL animated = ([self.view.layer animationForKey:@"bounds.size"] != nil); - [self.keypadView setButtonLabelHorizontalLayout:horizontalLayout animated:animated]; - - CGFloat topContentHeight = self.view.safeAreaInsets.top; - - // Layout the container view - frame = self.containerView.frame; - frame.origin.y = (((viewSize.height - (topContentHeight + self.contentOverlapFrame.size.height))) * 0.5f) - self.verticalMidPoint; - frame.origin.y += topContentHeight; - self.containerView.frame = CGRectIntegral(frame); - - // Layout the passcode options button - frame = self.optionsButton.frame; - frame.origin.y = CGRectGetMinY(self.contentOverlapFrame) - kTOPasscodeSettingsOptionsButtonOffset - CGRectGetHeight(frame); - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - self.optionsButton.frame = frame; - - // Set frame of title label - frame = self.titleLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - // Set frame of number pad - frame = self.inputField.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - frame.origin.y = (CGRectGetHeight(self.titleLabel.frame) + kTOPasscodeSettingsLabelInputSpacing); - self.inputField.frame = CGRectIntegral(frame); - - // Set the frame for the warning view - frame = self.warningLabel.frame; - frame.origin.x = (CGRectGetWidth(self.view.frame) - CGRectGetWidth(frame)) * 0.5f; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; - self.warningLabel.frame = CGRectIntegral(frame); - - // Set the frame of the error view - frame = self.errorLabel.frame; - frame.size = [self.errorLabel sizeThatFits:CGSizeMake(300.0f, CGFLOAT_MAX)]; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + kTOPasscodeSettingsLabelInputSpacing; - frame.origin.x = (CGRectGetWidth(self.containerView.frame) - CGRectGetWidth(frame)) * 0.5f; - self.errorLabel.frame = CGRectIntegral(frame); -} - -- (void)applyThemeForStyle:(TOPasscodeSettingsViewStyle)style -{ - BOOL isDark = (style == TOPasscodeSettingsViewStyleDark); - - // Set background color - UIColor *backgroundColor; - if (isDark) { - backgroundColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; - } - else { - backgroundColor = [UIColor colorWithRed:235.0f/255.0f green:235.0f/255.0f blue:241.0f/255.0f alpha:1.0f]; - } - self.view.backgroundColor = backgroundColor; - - // Set the style of the keypad view - self.keypadView.style = style; - - // Set the color for the input content - UIColor *inputColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - - // Set the label style - self.titleLabel.textColor = inputColor; - - // Set the number input tint - self.inputField.tintColor = inputColor; - - // Set the tint color of the incorrect warning label - UIColor *warningColor = nil; - if (isDark) { - warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - } - else { - warningColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - } -} - -#pragma mark - Data Management - -- (void)inputViewDidCompletePasscode:(NSString *)passcode -{ - switch (self.state) { - case TOPasscodeSettingsViewStateEnterCurrentPasscode: - [self validateCurrentPasscodeAttemptWithPasscode:passcode]; - break; - case TOPasscodeSettingsViewStateEnterNewPasscode: - [self didReceiveNewPasscode:passcode]; - break; - case TOPasscodeSettingsViewStateConfirmNewPasscode: - [self confirmNewPasscode:passcode]; - break; - } -} - -- (void)validateCurrentPasscodeAttemptWithPasscode:(NSString *)passcode -{ - if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didAttemptCurrentPasscode:)]) { - return; - } - - BOOL correct = [self.delegate passcodeSettingsViewController:self didAttemptCurrentPasscode:passcode]; - if (!correct) { - [self.inputField resetPasscodeAnimated:YES playImpact:YES]; - self.failedPasscodeAttemptCount++; - } - else { - [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; - } -} - -- (void)didReceiveNewPasscode:(NSString *)passcode -{ - self.potentialPasscode = passcode; - [self transitionToState:TOPasscodeSettingsViewStateConfirmNewPasscode animated:YES]; -} - -- (void)confirmNewPasscode:(NSString *)passcode -{ - if (![passcode isEqualToString:self.potentialPasscode]) { - [self transitionToState:TOPasscodeSettingsViewStateEnterNewPasscode animated:YES]; - self.errorLabel.hidden = NO; - return; - } - - if (![self.delegate respondsToSelector:@selector(passcodeSettingsViewController:didChangeToNewPasscode:ofType:)]) { - return; - } - - [self.delegate passcodeSettingsViewController:self didChangeToNewPasscode:self.potentialPasscode ofType:self.passcodeType]; -} - -#pragma mark - System Keyboard Handling - -- (void)keyboardWillChangeFrame:(NSNotification *)notification -{ - self.keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - [self viewDidLayoutSubviews]; -} - -- (CGRect)contentOverlapFrame -{ - if (self.passcodeType < TOPasscodeTypeCustomAlphanumeric) { - return self.keypadView.frame; - } - - // Work out where our view is in relation to the screen - UIWindow *window = self.view.window; - CGRect viewFrame = [self.view.superview convertRect:self.view.frame toView:window]; - - CGFloat overlap = CGRectGetMaxY(viewFrame) - CGRectGetMinY(self.keyboardFrame); - - CGRect overlapFrame = self.keyboardFrame; - overlapFrame.origin.y = MIN(viewFrame.size.height - overlap, viewFrame.size.height); - overlapFrame.size.height = MAX(overlap, 0.0f); - return overlapFrame; -} - -#pragma mark - Button Callbacks - - -- (void)optionsCodeButtonTapped:(id)sender -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - UIAlertActionStyle style = UIAlertActionStyleDefault; - - __weak typeof(self) weakSelf = self; - - NSArray *types = @[@(TOPasscodeTypeFourDigits), - @(TOPasscodeTypeSixDigits), - @(TOPasscodeTypeCustomNumeric), - @(TOPasscodeTypeCustomAlphanumeric) - ]; - - - NSArray *titles = @[NSLocalizedString(@"4-Digit Numeric Code", @""), - NSLocalizedString(@"6-Digit Numeric Code", @""), - NSLocalizedString(@"Custom Numeric Code", @""), - NSLocalizedString(@"Custom Alphanumeric Code", @"")]; - - // Add all the buttons - for (NSInteger i = 0; i < types.count; i++) { - TOPasscodeType type = [types[i] integerValue]; - if (type == self.passcodeType) { continue; } - - id handler = ^(UIAlertAction *action) { - [weakSelf setPasscodeType:type]; - }; - [alertController addAction:[UIAlertAction actionWithTitle:titles[i] style:style handler:handler]]; - } - - // Cancel button - [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleCancel handler:nil]]; - - alertController.modalPresentationStyle = UIModalPresentationPopover; - alertController.popoverPresentationController.sourceView = self.optionsButton; - alertController.popoverPresentationController.sourceRect = self.optionsButton.bounds; - alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionDown | UIPopoverArrowDirectionUp; - [self presentViewController:alertController animated:YES completion:nil]; -} - -- (void)nextButtonTapped:(id)sender -{ - [self inputViewDidCompletePasscode:self.inputField.passcode]; -} - -- (void)doneButtonTapped:(id)sender -{ - [self inputViewDidCompletePasscode:self.inputField.passcode]; -} - -#pragma mark - Accessors - -- (void)setPasscodeType:(TOPasscodeType)passcodeType -{ - [self setPasscodeType:passcodeType animated:NO]; -} - -- (void)setPasscodeType:(TOPasscodeType)passcodeType animated:(BOOL)animated -{ - if (_passcodeType == passcodeType) { return; } - _passcodeType = passcodeType; - - [self updateContentForState:self.state type:_passcodeType animated:animated]; -} - -- (void)setState:(TOPasscodeSettingsViewState)state -{ - if (_state == state) { return; } - _state = state; - - [self updateContentForState:_state type:self.passcodeType animated:NO]; -} - -- (void)setFailedPasscodeAttemptCount:(NSInteger)failedPasscodeAttemptCount -{ - if (_failedPasscodeAttemptCount == failedPasscodeAttemptCount) { return; } - _failedPasscodeAttemptCount = failedPasscodeAttemptCount; - [self updateWarningLabelForState:self.state]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h deleted file mode 100644 index f4f6a7023d..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.h +++ /dev/null @@ -1,163 +0,0 @@ -// -// TOPasscodeViewController.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import "TOPasscodeViewControllerConstants.h" -#import "TOPasscodeSettingsViewController.h" -#import "TOPasscodeView.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeViewController; - -/** - A delegate object in charge of validating the passcodes that the user has entered into the passcode - view controller. - */ -@protocol TOPasscodeViewControllerDelegate - -@optional - -/** - Return YES if the user entered the expected PIN code. Return NO if it was incorrect. - (For security reasons, it is safer to fetch the saved PIN code only when this method is called, and - then discard it immediately. This is why the view controller does not directly store it.) -*/ -- (BOOL)passcodeViewController:(TOPasscodeViewController *)passcodeViewController isCorrectCode:(NSString *)code; - -/** The user tapped the 'Cancel' button. Any dismissing of confidential content should be done in here. */ -- (void)didTapCancelInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** The user successfully entered the correct code, as validated by `isCorrectCode:` */ -- (void)didInputCorrectPasscodeInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** When available, the user tapped the 'Touch ID' button, or the view controller itself automatically initiated - the Touch ID request on display. This method is where you should implement your - own Touch ID validation logic. For security reasons, this controller does not implement the Touch ID logic itself. */ - -- (void)didPerformBiometricValidationRequestInPasscodeViewController:(TOPasscodeViewController *)passcodeViewController; - -/** Called when the pin view was resized as a result of the view controller being resized. - You can use this to resize your custom header view if necessary. - */ -- (void)passcodeViewController:(TOPasscodeViewController *)passcodeViewController didResizePasscodeViewToWidth:(CGFloat)width; - -@end - - -/** - A view controller that displays an interface for entering a user passcode. - It may be presented modally over another view controller, requiring the user to enter - the passcode correctly before they are able to proceed inside the application. - */ -@interface TOPasscodeViewController : UIViewController - -/** A delegate object, in charge of verifying the PIN code entered by the user */ -@property (nonatomic, weak, nullable) id delegate; - -/** The type of passcode that is expected to be entered. */ -@property (nonatomic, readonly) TOPasscodeType passcodeType; - -/** Will show a 'Touch ID' or 'Face ID' (depending on `biometricType`) button if the user is allowed to log in that way. (Default is NO) */ -@property (nonatomic, assign) BOOL allowBiometricValidation; - -/** Will handle delete button press as delete last symbol (Default is YES) */ -@property (nonatomic, assign) BOOL handleDeletePress; - -/** Set the type of biometrics for this device to update the title of the biometrics button properly. */ -@property (nonatomic, assign) TOPasscodeBiometryType biometryType; - -/** If biometrics are available, automatically ask for it upon presentation (Default is NO) */ -@property (nonatomic, assign) BOOL automaticallyPromptForBiometricValidation; - -/** Optionally change the color of the title text label. */ -@property (nonatomic, strong, nullable) UIColor *titleLabelColor; - -/** Optionally change the tint color of the UI element that indicates input progress (eg the row of circles) */ -@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; - -/** Optionally enable or disable showing the lettering label of all keypad circle buttons. **/ -@property (nonatomic, assign) BOOL keypadButtonShowLettering; - -/** If the style isn't translucent, changes the tint color of the keypad circle button outlines. */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundTintColor; - -/** The color of the text elements in each keypad button */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; - -/** Optionally, the text color of the keypad button text when tapped. Animates back to the base color. */ -@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; - -/** The tint button of the accessory button views at the bottom of the keypad (ie 'Cance' etc) */ -@property (nonatomic, strong, nullable) UIColor *accessoryButtonTintColor; - -/** Controls the transluceny of the PIN background when the style has been set to translucent. */ -@property (nonatomic, readonly) UIVisualEffectView *backgroundEffectView; - -/** Opaque, background view when the style is opaque */ -@property (nonatomic, readonly) UIView *backgroundView; - -/** The keypad and accessory views that are displayed in the center of this view */ -@property (nonatomic, readonly) TOPasscodeView *passcodeView; - -/** The Touch ID button, visible if biometrics is enabled and `leftAccessoryButton` is nil. */ -@property (nonatomic, readonly) UIButton *biometricButton; - -/** The Cancel, visible if `rightAccessoryButton` is nil. */ -@property (nonatomic, readonly) UIButton *cancelButton; - -/** The left accessory button. Setting this will override the 'Touch ID' button. */ -@property (nonatomic, strong, nullable) UIButton *leftAccessoryButton; - -/** The right accessory button. Setting this will override the 'Cancel' button. */ -@property (nonatomic, strong, nullable) UIButton *rightAccessoryButton; - -@property (nonatomic, assign) CGFloat accessoryButtonsVerticalInset; - -/** Whether all of the content views are hidden or not, but the background translucent view remains. - Useful for obscuring the content while the app is suspended. */ -@property (nonatomic, assign) BOOL contentHidden; - -/** - Create a new instance of this view controller with the preset style and passcode type. - - @param type The type of passcode to enter (6-digit/numeric) - */ -- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel; - -/** - Hide everything except the background translucency view. - - @param hidden Whether the content is hidden or not. - @param animated The content will play a crossfade animation. - */ -- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END - -//! Project version number for TOPasscodeViewController. -FOUNDATION_EXPORT double TOPasscodeViewControllerVersionNumber; - -//! Project version string for TOPasscodeViewController. -FOUNDATION_EXPORT const unsigned char TOPasscodeViewControllerVersionString[]; diff --git a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m b/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m deleted file mode 100755 index acbdabfcbe..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/TOPasscodeViewController.m +++ /dev/null @@ -1,711 +0,0 @@ -// -// TOPasscodeViewController.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeViewController.h" -#import "TOPasscodeView.h" -#import "TOPasscodeViewControllerAnimatedTransitioning.h" -#import "TOPasscodeKeypadView.h" -#import "TOPasscodeInputField.h" - -@interface TOPasscodeViewController () - -/* State */ -@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; -@property (nonatomic, assign) CGFloat keyboardHeight; -@property (nonatomic, assign) BOOL passcodeSuccess; -@property (nonatomic, readonly) UIView *leftButton; -@property (nonatomic, readonly) UIView *rightButton; - -/* Views */ -@property (nonatomic, strong, readwrite) UIVisualEffectView *backgroundEffectView; -@property (nonatomic, strong, readwrite) UIView *backgroundView; -@property (nonatomic, strong, readwrite) TOPasscodeView *passcodeView; -@property (nonatomic, strong, readwrite) UIButton *biometricButton; -@property (nonatomic, strong, readwrite) UIButton *cancelButton; - -/* Style */ -@property (nonatomic, assign) TOPasscodeViewStyle style; -@property (nonatomic, assign) BOOL allowCancel; - -@end - -@implementation TOPasscodeViewController - -#pragma mark - Instance Creation - - -- (instancetype)initPasscodeType:(TOPasscodeType)type allowCancel:(BOOL)cancel -{ - if (self = [super initWithNibName:nil bundle:nil]) { - _passcodeType = type; - _allowCancel = cancel; - [self setUp]; - } - - return self; -} - -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { - [self setUp]; - } - - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -#pragma mark - View Setup - - -- (void)setUp -{ - self.transitioningDelegate = self; - self.automaticallyPromptForBiometricValidation = NO; - self.handleDeletePress = YES; - - if (@available(iOS 13.0, *)) { - if ([self.traitCollection userInterfaceStyle] == UIUserInterfaceStyleDark) { - self.style = TOPasscodeViewStyleTranslucentDark; - } else { - self.style = TOPasscodeViewStyleTranslucentLight; - } - } else { - self.style = TOPasscodeViewStyleTranslucentLight; - } - - if (TOPasscodeViewStyleIsTranslucent(self.style)) { - self.modalPresentationStyle = UIModalPresentationOverFullScreen; - } - else { - self.modalPresentationStyle = UIModalPresentationFullScreen; - } - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) - name:UIKeyboardWillChangeFrameNotification object:nil]; -} - -- (void)setUpBackgroundEffectViewForStyle:(TOPasscodeViewStyle)style -{ - BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); - - // Return if it already exists when it should - if (translucent && self.backgroundEffectView) { return; } - - // Return if it doesn't exist when it shouldn't - if (!translucent && !self.backgroundEffectView) { return; } - - // Remove it if we're now opaque - if (!translucent) { - [self.backgroundEffectView removeFromSuperview]; - self.backgroundEffectView = nil; - return; - } - - // Create it otherwise - UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:[self blurEffectStyleForStyle:style]]; - self.backgroundEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; - self.backgroundEffectView.frame = self.view.bounds; - self.backgroundEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.view insertSubview:self.backgroundEffectView atIndex:0]; -} - -- (void)setUpBackgroundViewForStyle:(TOPasscodeViewStyle)style -{ - BOOL translucent = TOPasscodeViewStyleIsTranslucent(style); - - if (!translucent && self.backgroundView) { return; } - - if (translucent && !self.backgroundView) { return; } - - if (translucent) { - [self.backgroundView removeFromSuperview]; - self.backgroundView = nil; - return; - } - - self.backgroundView = [[UIView alloc] initWithFrame:self.view.bounds]; - self.backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self.view insertSubview:self.backgroundView atIndex:0]; -} - -- (UIBlurEffectStyle)blurEffectStyleForStyle:(TOPasscodeViewStyle)style -{ - switch (self.style) { - case TOPasscodeViewStyleTranslucentDark: return UIBlurEffectStyleDark; - case TOPasscodeViewStyleTranslucentLight: return UIBlurEffectStyleExtraLight; - default: return 0; - } - - return 0; -} - -- (void)setUpAccessoryButtons -{ - UIFont *buttonFont = [UIFont systemFontOfSize:16.0f]; - BOOL isPad = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; - - if (!self.leftAccessoryButton && self.allowBiometricValidation && !self.biometricButton) { - self.biometricButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(self.biometryType) forState:UIControlStateNormal]; - [self.biometricButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - - if (isPad) { - self.passcodeView.leftButton = self.biometricButton; - } - else { - [self.view addSubview:self.biometricButton]; - } - } - else { - if (self.leftAccessoryButton) { - [self.biometricButton removeFromSuperview]; - self.biometricButton = nil; - } - } - - if (!self.rightAccessoryButton && !self.cancelButton) { - self.cancelButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.cancelButton setTitle:NSLocalizedString(@"Cancel", @"Cancel") forState:UIControlStateNormal]; - self.cancelButton.titleLabel.font = buttonFont; - [self.cancelButton addTarget:self action:@selector(accessoryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - // If cancelling is disabled, we hide the cancel button but we still create it, because it can - // transition to backspace after user input. - self.cancelButton.hidden = !self.allowCancel; - if (isPad) { - self.passcodeView.rightButton = self.cancelButton; - } - else { - [self.view addSubview:self.cancelButton]; - } - } - else { - if (self.rightAccessoryButton) { - [self.cancelButton removeFromSuperview]; - self.cancelButton = nil; - } - } - - [self updateAccessoryButtonFontsForSize:self.view.bounds.size]; -} - -#pragma mark - View Management - -- (void)viewDidLoad -{ - [super viewDidLoad]; - self.view.backgroundColor = [UIColor clearColor]; - self.view.layer.allowsGroupOpacity = NO; - [self setUpBackgroundEffectViewForStyle:self.style]; - [self setUpBackgroundViewForStyle:self.style]; - [self setUpAccessoryButtons]; - [self applyThemeForStyle:self.style]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - // Automatically trigger biometric validation if available - if (self.allowBiometricValidation && self.automaticallyPromptForBiometricValidation) { - [self accessoryButtonTapped:self.biometricButton]; - } -} - -- (void)viewDidLayoutSubviews -{ - CGSize bounds = self.view.bounds.size; - CGSize maxSize = bounds; - if (@available(iOS 11.0, *)) { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; - if (safeAreaInsets.bottom > 0) { - maxSize.height -= safeAreaInsets.bottom; - } - if (safeAreaInsets.left > 0) { - maxSize.width -= safeAreaInsets.left; - } - if (safeAreaInsets.right > 0) { - maxSize.width -= safeAreaInsets.right; - } - } - - // Resize the pin view to scale to the new size - [self.passcodeView sizeToFitSize:maxSize]; - - // Re-center the pin view - CGRect frame = self.passcodeView.frame; - frame.origin.x = (bounds.width - frame.size.width) * 0.5f; - frame.origin.y = ((bounds.height - self.keyboardHeight) - frame.size.height) * 0.5f; - self.passcodeView.frame = CGRectIntegral(frame); - - // -------------------------------------------------- - - // Update the accessory button sizes - [self updateAccessoryButtonFontsForSize:maxSize]; - - // Re-layout the accessory buttons - [self layoutAccessoryButtonsForSize:maxSize]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - [self setNeedsStatusBarAppearanceUpdate]; - - // Force an initial layout if the view hasn't been presented yet - [UIView performWithoutAnimation:^{ - [self.view setNeedsLayout]; - [self.view layoutIfNeeded]; - }]; - - // Show the keyboard if we're entering alphanumeric characters - if (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { - [self.passcodeView.inputField becomeFirstResponder]; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - // Dismiss the keyboard if it is visible - if (self.passcodeView.inputField.isFirstResponder) { - [self.passcodeView.inputField resignFirstResponder]; - } -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return TOPasscodeViewStyleIsDark(self.style) ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; -} - -#pragma mark - View Rotations - -- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator -{ - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - - // We don't need to do anything special on iPad or if we're using character input - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad || self.passcodeType == TOPasscodeTypeCustomAlphanumeric) { return; } - - // Work out if we need to transition to horizontal - BOOL horizontalLayout = size.height < size.width; - - // Perform layout animation - [self.passcodeView setHorizontalLayout:horizontalLayout animated:coordinator.animated duration:coordinator.transitionDuration]; -} - -#pragma mark - View Styling - -- (void)applyThemeForStyle:(TOPasscodeViewStyle)style -{ - BOOL isDark = TOPasscodeViewStyleIsDark(style); - - // Apply the tint color to the accessory buttons - UIColor *accessoryTintColor = self.accessoryButtonTintColor; - if (!accessoryTintColor) { - accessoryTintColor = isDark ? [UIColor whiteColor] : nil; - } - - self.biometricButton.tintColor = accessoryTintColor; - self.cancelButton.tintColor = accessoryTintColor; - self.leftAccessoryButton.tintColor = accessoryTintColor; - self.rightAccessoryButton.tintColor = accessoryTintColor; - - self.backgroundView.backgroundColor = isDark ? [UIColor colorWithWhite:0.1f alpha:1.0f] : [UIColor whiteColor]; -} - -- (void)updateAccessoryButtonFontsForSize:(CGSize)size -{ - CGFloat width = size.width; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { - width = MIN(size.width, size.height); - } - - CGFloat pointSize = 17.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - pointSize = 14.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - pointSize = 16.0f; - } - - UIFont *accessoryFont = [UIFont systemFontOfSize:pointSize]; - - self.biometricButton.titleLabel.font = accessoryFont; - self.cancelButton.titleLabel.font = accessoryFont; - self.leftAccessoryButton.titleLabel.font = accessoryFont; - self.rightAccessoryButton.titleLabel.font = accessoryFont; -} - -- (void)verticalLayoutAccessoryButtonsForSize:(CGSize)size -{ - CGFloat width = MIN(size.width, size.height); - - CGFloat verticalInset = 44.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - verticalInset = 20.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - verticalInset = 30.0f; - } - - if (self.accessoryButtonsVerticalInset > 0) { - verticalInset = self.accessoryButtonsVerticalInset; - } - - CGFloat inset = self.passcodeView.keypadButtonInset; - CGPoint point = (CGPoint){0.0f, (self.view.bounds.size.height - self.keyboardHeight) - verticalInset}; - if (@available(iOS 11.0, *)) { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; - if (safeAreaInsets.bottom > 0) { - point.y -= safeAreaInsets.bottom; - } - } - - if (self.leftButton) { - [self.leftButton sizeToFit]; - point.x = self.passcodeView.frame.origin.x + inset; - self.leftButton.center = point; - } - - if (self.rightButton) { - [self.rightButton sizeToFit]; - point.x = CGRectGetMaxX(self.passcodeView.frame) - inset; - self.rightButton.center = point; - } -} - -- (void)horizontalLayoutAccessoryButtonsForSize:(CGSize)size -{ - CGRect passcodeViewFrame = self.passcodeView.frame; - CGFloat buttonInset = self.passcodeView.keypadButtonInset; - CGFloat width = MIN(size.width, size.height); - CGFloat verticalInset = 35.0f; - if (width < (CGFloat)TOPasscodeViewContentSizeMedium) { - verticalInset = 30.0f; - } - else if (width < (CGFloat)TOPasscodeViewContentSizeDefault) { - verticalInset = 35.0f; - } - - if (self.leftButton) { - [self.leftButton sizeToFit]; - CGRect frame = self.leftButton.frame; - frame.origin.y = (self.view.bounds.size.height - verticalInset) - (frame.size.height * 0.5f); - frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); - self.leftButton.frame = CGRectIntegral(frame); - } - - if (self.rightButton) { - [self.rightButton sizeToFit]; - CGRect frame = self.rightButton.frame; - frame.origin.y = verticalInset - (frame.size.height * 0.5f); - frame.origin.x = (CGRectGetMaxX(passcodeViewFrame) - buttonInset) - (frame.size.width * 0.5f); - self.rightButton.frame = CGRectIntegral(frame); - } - - [self.view bringSubviewToFront:self.rightButton]; - [self.view bringSubviewToFront:self.leftButton]; -} - -- (void)layoutAccessoryButtonsForSize:(CGSize)size -{ - // The buttons are always embedded in the keypad view on iPad - if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPhone) { return; } - - if (self.passcodeView.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - [self horizontalLayoutAccessoryButtonsForSize:size]; - } - else { - [self verticalLayoutAccessoryButtonsForSize:size]; - } -} - -#pragma mark - Interactions - -- (void)accessoryButtonTapped:(id)sender -{ - if (sender == self.cancelButton) { - // When entering keyboard input, just leave the button as 'cancel' - if (self.handleDeletePress && self.passcodeType != TOPasscodeTypeCustomAlphanumeric && self.passcodeView.passcode.length > 0) { - [self.passcodeView deleteLastPasscodeCharacterAnimated:YES]; - [self keypadButtonTapped]; - return; - } - - if ([self.delegate respondsToSelector:@selector(didTapCancelInPasscodeViewController:)]) { - [self.delegate didTapCancelInPasscodeViewController:self]; - } - } - else if (sender == self.biometricButton) { - if ([self.delegate respondsToSelector:@selector(didPerformBiometricValidationRequestInPasscodeViewController:)]) { - [self.delegate didPerformBiometricValidationRequestInPasscodeViewController:self]; - } - } -} - -- (void)keypadButtonTapped -{ - NSString *title = nil; - if (self.passcodeView.passcode.length > 0) { - title = NSLocalizedString(@"Delete", @"Delete"); - } else if (self.allowCancel) { - title = NSLocalizedString(@"Cancel", @"Cancel"); - } - [UIView performWithoutAnimation:^{ - if (title != nil) { - [self.cancelButton setTitle:title forState:UIControlStateNormal]; - [self.cancelButton layoutIfNeeded]; - } - self.cancelButton.hidden = (title == nil); - }]; -} - -- (void)didCompleteEnteringPasscode:(NSString *)passcode -{ - if (![self.delegate respondsToSelector:@selector(passcodeViewController:isCorrectCode:)]) { - return; - } - - // Validate the code - BOOL isCorrect = [self.delegate passcodeViewController:self isCorrectCode:passcode]; - if (!isCorrect) { - [self.passcodeView resetPasscodeAnimated:YES playImpact:YES]; - return; - } - - // Hang onto the fact the passcode was successful to play a nicer dismissal animation - self.passcodeSuccess = YES; - - // Perform handler if correctly entered - if ([self.delegate respondsToSelector:@selector(didInputCorrectPasscodeInPasscodeViewController:)]) { - [self.delegate didInputCorrectPasscodeInPasscodeViewController:self]; - } - else { - [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; - } -} - -#pragma mark - Keyboard Handling - -- (void)keyboardWillChangeFrame:(NSNotification *)notification -{ - // Extract the keyboard information we need from the notification - CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; - CGFloat animationDuration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue]; - UIViewAnimationOptions animationCurve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; - - // Work out the on-screen height of the keyboard - self.keyboardHeight = self.view.bounds.size.height - keyboardFrame.origin.y; - self.keyboardHeight = MAX(self.keyboardHeight, 0.0f); - - // Set that the view needs to be laid out - [self.view setNeedsLayout]; - - if (animationDuration < FLT_EPSILON) { - return; - } - - // Animate the content sliding up and down with the keyboard - [UIView animateWithDuration:animationDuration - delay:0.0f - options:animationCurve - animations:^{ [self.view layoutIfNeeded]; } - completion:nil]; -} - -#pragma mark - Transitioning Delegate - -- (nullable id )animationControllerForPresentedController:(UIViewController *)presented - presentingController:(UIViewController *)presenting - sourceController:(UIViewController *)source -{ - return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:NO success:NO]; -} - -- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed -{ - return [[TOPasscodeViewControllerAnimatedTransitioning alloc] initWithPasscodeViewController:self dismissing:YES success:self.passcodeSuccess]; -} - -#pragma mark - Convenience Accessors - -- (UIView *)leftButton -{ - return self.leftAccessoryButton ? self.leftAccessoryButton : self.biometricButton; -} - -- (UIView *)rightButton -{ - return self.rightAccessoryButton ? self.rightAccessoryButton : self.cancelButton; -} - -#pragma mark - Public Accessors - -- (TOPasscodeView *)passcodeView -{ - if (_passcodeView) { return _passcodeView; } - - _passcodeView = [[TOPasscodeView alloc] initWithStyle:self.style passcodeType:self.passcodeType]; - _passcodeView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin | - UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [_passcodeView sizeToFit]; - _passcodeView.center = self.view.center; - [self.view addSubview:_passcodeView]; - - __weak typeof(self) weakSelf = self; - _passcodeView.passcodeCompletedHandler = ^(NSString *passcode) { - [weakSelf didCompleteEnteringPasscode:passcode]; - }; - - _passcodeView.passcodeDigitEnteredHandler = ^{ - [weakSelf keypadButtonTapped]; - }; - - // Set initial layout to horizontal if we're rotated on an iPhone - if (self.passcodeType != TOPasscodeTypeCustomAlphanumeric && [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { - CGSize boundsSize = self.view.bounds.size; - _passcodeView.horizontalLayout = boundsSize.width > boundsSize.height; - } - - return _passcodeView; -} - -- (void)setStyle:(TOPasscodeViewStyle)style -{ - if (style == _style) { return; } - _style = style; - - self.passcodeView.style = style; - [self setUpBackgroundEffectViewForStyle:style]; -} - -- (void)setAllowBiometricValidation:(BOOL)allowBiometricValidation -{ - if (_allowBiometricValidation == allowBiometricValidation) { - return; - } - - _allowBiometricValidation = allowBiometricValidation; - [self setUpAccessoryButtons]; - [self applyThemeForStyle:self.style]; -} - -- (void)setTitleLabelColor:(UIColor *)titleLabelColor -{ - self.passcodeView.titleLabelColor = titleLabelColor; -} - -- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor -{ - self.passcodeView.subtitleLabelColor = subtitleLabelColor; -} - -- (UIColor *)titleLabelColor { return self.passcodeView.titleLabelColor; } - -- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor -{ - self.passcodeView.inputProgressViewTintColor = inputProgressViewTintColor; -} - -- (UIColor *)inputProgressViewTintColor { return self.passcodeView.inputProgressViewTintColor; } - -- (void)setKeypadButtonBackgroundTintColor:(UIColor *)keypadButtonBackgroundTintColor -{ - self.passcodeView.keypadButtonBackgroundColor = keypadButtonBackgroundTintColor; -} - -- (void)setKeypadButtonShowLettering:(BOOL)keypadButtonShowLettering -{ - self.passcodeView.keypadView.showLettering = keypadButtonShowLettering; -} - -- (UIColor *)keypadButtonBackgroundTintColor { return self.passcodeView.keypadButtonBackgroundColor; } - -- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor -{ - self.passcodeView.keypadButtonTextColor = keypadButtonTextColor; -} - -- (UIColor *)keypadButtonTextColor { return self.passcodeView.keypadButtonTextColor; } - -- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor -{ - self.passcodeView.keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; -} - -- (UIColor *)keypadButtonHighlightedTextColor { return self.passcodeView.keypadButtonHighlightedTextColor; } - -- (void)setAccessoryButtonTintColor:(UIColor *)accessoryButtonTintColor -{ - if (accessoryButtonTintColor == _accessoryButtonTintColor) { return; } - _accessoryButtonTintColor = accessoryButtonTintColor; - [self applyThemeForStyle:self.style]; -} - -- (void)setBiometryType:(TOPasscodeBiometryType)biometryType -{ - if (_biometryType == biometryType) { return; } - - _biometryType = biometryType; - - if (self.biometricButton) { - [self.biometricButton setTitle:TOPasscodeBiometryTitleForType(_biometryType) forState:UIControlStateNormal]; - } -} - -- (void)setContentHidden:(BOOL)contentHidden -{ - [self setContentHidden:contentHidden animated:NO]; -} - -- (void)setContentHidden:(BOOL)hidden animated:(BOOL)animated -{ - if (hidden == _contentHidden) { return; } - _contentHidden = hidden; - - void (^setViewsHiddenBlock)(BOOL) = ^(BOOL hidden) { - self.passcodeView.hidden = hidden; - self.leftButton.hidden = hidden; - self.rightButton.hidden = hidden; - }; - - void (^completionBlock)(BOOL) = ^(BOOL complete) { - setViewsHiddenBlock(hidden); - }; - - if (!animated) { - completionBlock(YES); - return; - } - - // Make sure the views are visible before the animation - setViewsHiddenBlock(NO); - - void (^animationBlock)(void) = ^{ - CGFloat alpha = hidden ? 0.0f : 1.0f; - self.passcodeView.contentAlpha = alpha; - self.leftButton.alpha = alpha; - self.rightButton.alpha = alpha; - }; - - // Animate - [UIView animateWithDuration:0.4f animations:animationBlock completion:completionBlock]; -} - -@end - diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h deleted file mode 100644 index b44bfc7dc1..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.h +++ /dev/null @@ -1,81 +0,0 @@ -// -// TOPasscodeCircleButton.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -@class TOPasscodeCircleView; -@class TOPasscodeButtonLabel; - -NS_ASSUME_NONNULL_BEGIN - -/** - A UI control representing a single PIN code button for the keypad, - including the number, lettering (eg 'ABC'), and circle border. - */ -@interface TOPasscodeCircleButton : UIControl - -// Alpha value that properly controls the necessary subviews -@property (nonatomic, assign) CGFloat contentAlpha; - -// Required to be set before this view can be properly rendered -@property (nonatomic, strong) UIImage *backgroundImage; -@property (nonatomic, strong) UIImage *hightlightedBackgroundImage; -@property (nonatomic, strong) UIVibrancyEffect *vibrancyEffect; - -// Properties with default values -@property (nonatomic, strong) UIColor *textColor; -@property (nonatomic, strong, nullable) UIColor *highlightedTextColor; -@property (nonatomic, strong) UIFont *numberFont; -@property (nonatomic, strong) UIFont *letteringFont; -@property (nonatomic, assign) CGFloat letteringCharacterSpacing; -@property (nonatomic, assign) CGFloat letteringVerticalSpacing; - -@property (nonatomic, readonly) NSString *numberString; -@property (nonatomic, readonly) NSString *letteringString; - -// The internal views -@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; -@property (nonatomic, readonly) TOPasscodeCircleView *circleView; -@property (nonatomic, readonly) UIVisualEffectView *vibrancyView; - -// Callback handler -@property (nonatomic, copy) void (^buttonTappedHandler)(void); - -/** - Create a new instance of the class with the supplied number and lettering string - - @param numberString The string of the number to display in this button (eg '1'). - @param letteringString The string of the lettering to display underneath. - */ -- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString; - -/** - Set the background of the button to be the filled circle instead of hollow. - - @param highlighted When YES, the circle is full, when NO, it is hollow. - @param animated When animated, the transition is a crossfade. - */ -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m deleted file mode 100644 index 790bfe0db3..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeCircleButton.m +++ /dev/null @@ -1,236 +0,0 @@ -// -// TOPasscodeCircleButton.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeCircleButton.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeCircleButton () - -@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; -@property (nonatomic, strong, readwrite) TOPasscodeCircleView *circleView; -@property (nonatomic, strong, readwrite) UIVisualEffectView *vibrancyView; - -@property (nonatomic, readwrite, copy) NSString *numberString; -@property (nonatomic, readwrite, copy) NSString *letteringString; - -@end - -@implementation TOPasscodeCircleButton - -- (instancetype)initWithNumberString:(NSString *)numberString letteringString:(NSString *)letteringString -{ - if (self = [super init]) { - _numberString = numberString; - _letteringString = letteringString; - _contentAlpha = 1.0f; - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - self.userInteractionEnabled = YES; - - _textColor = [UIColor whiteColor]; - - [self setUpSubviews]; - [self setUpViewInteraction]; -} - -- (void)setUpSubviews -{ - if (!self.circleView) { - self.circleView = [[TOPasscodeCircleView alloc] initWithFrame:self.bounds]; - [self addSubview:self.circleView]; - } - - if (!self.buttonLabel) { - self.buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:self.bounds]; - self.buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.buttonLabel.userInteractionEnabled = NO; - self.buttonLabel.textColor = self.textColor; - self.buttonLabel.numberString = self.numberString; - self.buttonLabel.letteringString = self.letteringString; - [self addSubview:self.buttonLabel]; - } - - if (!self.vibrancyView) { - self.vibrancyView = [[UIVisualEffectView alloc] initWithEffect:nil]; - self.vibrancyView.userInteractionEnabled = NO; - [self.vibrancyView.contentView addSubview:self.circleView]; - [self addSubview:self.vibrancyView]; - } -} - -- (void)setUpViewInteraction -{ - if (self.allTargets.count) { return; } - - [self addTarget:self action:@selector(buttonDidTouchDown:) forControlEvents:UIControlEventTouchDown]; - [self addTarget:self action:@selector(buttonDidTouchUpInside:) forControlEvents:UIControlEventTouchUpInside]; - [self addTarget:self action:@selector(buttonDidDragInside:) forControlEvents:UIControlEventTouchDragEnter]; - [self addTarget:self action:@selector(buttonDidDragOutside:) forControlEvents:UIControlEventTouchDragExit]; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - self.vibrancyView.frame = self.bounds; - self.circleView.frame = self.vibrancyView ? self.vibrancyView.bounds : self.bounds; - self.buttonLabel.frame = self.bounds; - [self bringSubviewToFront:self.buttonLabel]; -} - -#pragma mark - User Interaction - - -- (void)buttonDidTouchDown:(id)sender -{ - if (self.buttonTappedHandler) { self.buttonTappedHandler(); } - [self setHighlighted:YES animated:NO]; -} - -- (void)buttonDidTouchUpInside:(id)sender { [self setHighlighted:NO animated:YES]; } -- (void)buttonDidDragInside:(id)sender { [self setHighlighted:YES animated:NO]; } -- (void)buttonDidDragOutside:(id)sender { [self setHighlighted:NO animated:YES]; } - -#pragma mark - Animated Accessors - - -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated -{ - [self.circleView setHighlighted:highlighted animated:animated]; - - if (!self.highlightedTextColor) { return; } - - void (^textFadeBlock)(void) = ^{ - self.buttonLabel.textColor = highlighted ? self.highlightedTextColor : self.textColor; - }; - - if (!animated) { - textFadeBlock(); - return; - } - - [UIView transitionWithView:self.buttonLabel - duration:0.6f - options:UIViewAnimationOptionTransitionCrossDissolve - animations:textFadeBlock - completion:nil]; -} - -#pragma mark - Accessors - - -- (void)setBackgroundImage:(UIImage *)backgroundImage -{ - self.circleView.circleImage = backgroundImage; - CGRect frame = self.frame; - frame.size = backgroundImage.size; - self.frame = CGRectIntegral(frame); -} - -- (UIImage *)backgroundImage { return self.circleView.circleImage; } - -/***********************************************************/ - -- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect -{ - if (_vibrancyEffect == vibrancyEffect) { return; } - _vibrancyEffect = vibrancyEffect; - self.vibrancyView.effect = _vibrancyEffect; -} - -/***********************************************************/ - -- (void)setHightlightedBackgroundImage:(UIImage *)hightlightedBackgroundImage -{ - self.circleView.highlightedCircleImage = hightlightedBackgroundImage; -} - -- (UIImage *)hightlightedBackgroundImage { return self.circleView.highlightedCircleImage; } - -/***********************************************************/ - -- (void)setNumberFont:(UIFont *)numberFont -{ - self.buttonLabel.numberLabelFont = numberFont; - [self setNeedsLayout]; -} - -- (UIFont *)numberFont { return self.buttonLabel.numberLabelFont; } - -/***********************************************************/ - -- (void)setLetteringFont:(UIFont *)letteringFont -{ - self.buttonLabel.letteringLabelFont = letteringFont; - [self setNeedsLayout]; -} - -- (UIFont *)letteringFont { return self.buttonLabel.letteringLabelFont; } - -/***********************************************************/ - -- (void)setLetteringVerticalSpacing:(CGFloat)letteringVerticalSpacing -{ - self.buttonLabel.letteringVerticalSpacing = letteringVerticalSpacing; - [self.buttonLabel setNeedsLayout]; -} - -- (CGFloat)letteringVerticalSpacing { return self.buttonLabel.letteringVerticalSpacing; } - -/***********************************************************/ - -- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing -{ - self.buttonLabel.letteringCharacterSpacing = letteringCharacterSpacing; -} - -- (CGFloat)letteringCharacterSpacing { return self.buttonLabel.letteringCharacterSpacing; } - -/***********************************************************/ - -- (void)setTextColor:(UIColor *)textColor -{ - if (textColor == _textColor) { return; } - _textColor = textColor; - - self.buttonLabel.textColor = _textColor; -} - -/***********************************************************/ - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - if (_contentAlpha == contentAlpha) { - return; - } - - _contentAlpha = contentAlpha; - - self.buttonLabel.alpha = contentAlpha; - self.circleView.alpha = contentAlpha; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h deleted file mode 100644 index 4db6bb9a88..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.h +++ /dev/null @@ -1,101 +0,0 @@ -// -// TOPasscodeKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeCircleButton; - -/** - A view encompassing 9 circle buttons, making up a keypad view for entering PIN numbers. - Can be laid out vertically or horizontally. - */ -@interface TOPasscodeKeypadView : UIView - -/** The type of layout for the buttons (Default is vertical) */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/** The vibrancy effect to be applied to each button background */ -@property (nonatomic, strong, nullable) UIVibrancyEffect *vibrancyEffect; - -/** The size of each input button */ -@property (nonatomic, assign) CGFloat buttonDiameter; - -/** The stroke width of the buttons */ -@property (nonatomic, assign) CGFloat buttonStrokeWidth; - -/** The spacing between the buttons. Default is (CGSize){25,15} */ -@property (nonatomic, assign) CGSize buttonSpacing; - -/** The font of the number in each button */ -@property (nonatomic, strong) UIFont *buttonNumberFont; - -/** The font of the lettering label */ -@property (nonatomic, strong) UIFont *buttonLetteringFont; - -/** The spacing between the lettering and the number label */ -@property (nonatomic, assign) CGFloat buttonLabelSpacing; - -/** The spacing between the letters in the lettering label */ -@property (nonatomic, assign) CGFloat buttonLetteringSpacing; - -/** Show the 'ABC' lettering under the numbers */ -@property (nonatomic, assign) BOOL showLettering; - -/** The spacing in points between the letters */ -@property (nonatomic, assign) CGFloat letteringSpacing; - -/** The tint color of the button backgrounds */ -@property (nonatomic, strong) UIColor *buttonBackgroundColor; - -/** The color of the text elements in each button */ -@property (nonatomic, strong) UIColor *buttonTextColor; - -/** Optionally the color of text when it's tapped. */ -@property (nonatomic, strong, nullable) UIColor *buttonHighlightedTextColor; - -/** The alpha value of all non-translucent views */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/** Accessory views placed on either side of the '0' button */ -@property (nonatomic, strong, nullable) UIView *leftAccessoryView; -@property (nonatomic, strong, nullable) UIView *rightAccessoryView; - -/** The controls making up each of the button views */ -@property (nonatomic, readonly) NSArray *keypadButtons; - -/** The block that is triggered whenever a user taps one of the buttons */ -@property (nonatomic, copy) void (^buttonTappedHandler)(NSInteger buttonNumber); - -/* - Perform an animation to transition to a new layout. - - @param horizontalLayout The content is laid out horizontally. - @param animated Whether the transition is animated - @param duration The animation length of the transition. - */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m deleted file mode 100644 index a9cf0bb511..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeKeypadView.m +++ /dev/null @@ -1,432 +0,0 @@ -// -// TOPasscodeKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeKeypadView.h" -#import "TOPasscodeCircleImage.h" -#import "TOPasscodeCircleButton.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeKeypadView() - -/* Passcode buttons */ -@property (nonatomic, strong, readwrite) NSArray *keypadButtons; - -/* The '0' button for the different layouts */ -@property (nonatomic, strong) TOPasscodeCircleButton *verticalZeroButton; -@property (nonatomic, strong) TOPasscodeCircleButton *horizontalZeroButton; - -/* Images */ -@property (nonatomic, strong) UIImage *buttonImage; -@property (nonatomic, strong) UIImage *tappedButtonImage; - -@end - -@implementation TOPasscodeKeypadView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - self.userInteractionEnabled = YES; - _buttonDiameter = 81.0f; - _buttonSpacing = (CGSize){25,15}; - _buttonStrokeWidth = 1.5f; - _showLettering = YES; - _buttonNumberFont = nil; - _buttonLetteringFont = nil; - _buttonLabelSpacing = FLT_MIN; - _buttonLetteringSpacing = FLT_MIN; - [self sizeToFit]; - } - - return self; -} - -- (TOPasscodeCircleButton *)makeCircleButtonWithNumber:(NSInteger)number letteringString:(NSString *)letteringString -{ - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)number]; - - TOPasscodeCircleButton *circleButton = [[TOPasscodeCircleButton alloc] initWithNumberString:numberString letteringString:letteringString]; - circleButton.backgroundImage = self.buttonImage; - circleButton.hightlightedBackgroundImage = self.tappedButtonImage; - circleButton.vibrancyEffect = self.vibrancyEffect; - - // Add handler for when button is tapped - __weak typeof(self) weakSelf = self; - circleButton.buttonTappedHandler = ^{ - if (weakSelf.buttonTappedHandler) { - weakSelf.buttonTappedHandler(number); - } - }; - - return circleButton; -} - -- (void)setUpButtons -{ - NSMutableArray *buttons = [NSMutableArray array]; - - NSInteger numberOfButtons = 11; // 1-9 are normal, 10 is the vertical '0', 11 is the horizontal '0' - NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", - @"MNO", @"PQRS", @"TUV", @"WXYZ"]; - - for (NSInteger i = 0; i < numberOfButtons; i++) { - // Work out the button number text - NSInteger buttonNumber = i + 1; - if (buttonNumber == 10 || buttonNumber == 11) { buttonNumber = 0; } - - // Work out the lettering text - NSString *letteringString = nil; - if (self.showLettering && i > 0 && i-1 < letteredTitles.count) { // (Skip 1 and 0) - letteringString = letteredTitles[i-1]; - } - - // Create a new button and add it to this view - TOPasscodeCircleButton *circleButton = [self makeCircleButtonWithNumber:buttonNumber letteringString:letteringString]; - [self addSubview:circleButton]; - [buttons addObject:circleButton]; - - if (!self.showLettering) { - circleButton.buttonLabel.verticallyCenterNumberLabel = YES; // Center the digit in the middle - } - - // Hang onto the 0 button if it's the vertical one - // And center the text - if (i == 9) { - self.verticalZeroButton = circleButton; - - // Hide the button if it's not vertically laid out - if (self.horizontalLayout) { - self.verticalZeroButton.contentAlpha = 0.0f; - self.verticalZeroButton.hidden = YES; - } - } - else if (i == 10) { - self.horizontalZeroButton = circleButton; - - // Hide the button if it's not horizontally laid out - if (!self.horizontalLayout) { - self.horizontalZeroButton.contentAlpha = 0.0f; - self.horizontalZeroButton.hidden = YES; - } - } - } - - _keypadButtons = [NSArray arrayWithArray:buttons]; -} - -- (void)sizeToFit -{ - CGFloat padding = 2.0f; - - CGRect frame = self.frame; - if (self.horizontalLayout) { - frame.size.width = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.width * 3); - frame.size.height = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.height * 2); - } - else { - frame.size.width = ((self.buttonDiameter + padding) * 3) + (self.buttonSpacing.width * 2); - frame.size.height = ((self.buttonDiameter + padding) * 4) + (self.buttonSpacing.height * 3); - } - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - NSInteger i = 0; - CGPoint origin = CGPointZero; - for (TOPasscodeCircleButton *button in self.keypadButtons) { - // Set the button frame - CGRect frame = button.frame; - frame.origin = origin; - button.frame = frame; - - // Work out the next offset - CGFloat horizontalOffset = frame.size.width + self.buttonSpacing.width; - origin.x += horizontalOffset; - - i++; - - // If we're at the end of the row, move to the next one - if (i % 3 == 0) { - origin.x = 0.0f; - origin.y = origin.y + frame.size.height + self.buttonSpacing.height; - } - } - - // Lay out the vertical button - CGRect frame = self.verticalZeroButton.frame; - frame.origin.x += (frame.size.width + self.buttonSpacing.width); - self.verticalZeroButton.frame = frame; - - // Lay out the horizontal button - frame = self.horizontalZeroButton.frame; - frame.origin.x = (frame.size.width + self.buttonSpacing.width) * 3.0f; - frame.origin.y = frame.size.height + self.buttonSpacing.height; - self.horizontalZeroButton.frame = frame; - - // Layout the accessory buttons - CGFloat midPointY = CGRectGetMidY(self.verticalZeroButton.frame); - - if (self.leftAccessoryView) { - CGRect leftButtonFrame = self.keypadButtons.firstObject.frame; - CGFloat midPointX = CGRectGetMidX(leftButtonFrame); - - [self.leftAccessoryView sizeToFit]; - self.leftAccessoryView.center = (CGPoint){midPointX, midPointY}; - } - - if (self.rightAccessoryView) { - CGRect rightButtonFrame = self.keypadButtons[2].frame; - CGFloat midPointX = CGRectGetMidX(rightButtonFrame); - - [self.rightAccessoryView sizeToFit]; - self.rightAccessoryView.center = (CGPoint){midPointX, midPointY}; - } -} - -#pragma mark - Style Accessors - -- (void)setVibrancyEffect:(UIVibrancyEffect *)vibrancyEffect -{ - if (vibrancyEffect == _vibrancyEffect) { return; } - _vibrancyEffect = vibrancyEffect; - - for (TOPasscodeCircleButton *button in self.keypadButtons) { - button.vibrancyEffect = _vibrancyEffect; - } -} - -#pragma mark - Lazy Getters - -- (UIImage *)buttonImage -{ - if (!_buttonImage) { - _buttonImage = [TOPasscodeCircleImage hollowCircleImageOfSize:self.buttonDiameter strokeWidth:self.buttonStrokeWidth padding:1.0f]; - } - - return _buttonImage; -} - -- (UIImage *)tappedButtonImage -{ - if (!_tappedButtonImage) { - _tappedButtonImage = [TOPasscodeCircleImage circleImageOfSize:self.buttonDiameter inset:self.buttonStrokeWidth * 0.5f padding:1.0f antialias:YES]; - } - - return _tappedButtonImage; -} - -- (NSArray *)keypadButtons -{ - if (_keypadButtons) { return _keypadButtons; } - [self setUpButtons]; - return _keypadButtons; -} - -#pragma mark - Audio Delegate Protocol - -- (BOOL)enableInputClicksWhenVisible -{ - return YES; -} - -#pragma mark - Public Layout Setters - - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (horizontalLayout== _horizontalLayout) { - return; - } - - _horizontalLayout = horizontalLayout; - - // Resize itself now so the frame value is up to date externally - [self sizeToFit]; - - // Set initial animation state - self.verticalZeroButton.hidden = NO; - self.horizontalZeroButton.hidden = NO; - - self.verticalZeroButton.contentAlpha = _horizontalLayout ? 1.0f : 0.0f; - self.horizontalZeroButton.contentAlpha = _horizontalLayout ? 0.0f : 1.0f; - - void (^animationBlock)(void) = ^{ - self.verticalZeroButton.contentAlpha = self.horizontalLayout ? 0.0f : 1.0f; - self.horizontalZeroButton.contentAlpha = self.horizontalLayout ? 1.0f : 0.0f; - }; - - void (^completionBlock)(BOOL) = ^(BOOL complete) { - self.verticalZeroButton.hidden = self.horizontalLayout; - self.horizontalZeroButton.hidden = self.horizontalLayout; - }; - - // Don't animate if not needed - if (!animated) { - animationBlock(); - completionBlock(YES); - return; - } - - // Perform animation - [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; -} - -- (void)updateButtonsForCurrentState -{ - for (TOPasscodeCircleButton *circleButton in self.keypadButtons) { - circleButton.backgroundImage = self.buttonImage; - circleButton.hightlightedBackgroundImage = self.tappedButtonImage; - circleButton.numberFont = self.buttonNumberFont; - circleButton.letteringFont = self.buttonLetteringFont; - circleButton.letteringVerticalSpacing = self.buttonLabelSpacing; - circleButton.letteringCharacterSpacing = self.buttonLetteringSpacing; - circleButton.tintColor = self.buttonBackgroundColor; - circleButton.textColor = self.buttonTextColor; - circleButton.highlightedTextColor = self.buttonHighlightedTextColor; - if (!_showLettering) { - circleButton.buttonLabel.letteringLabel.text = nil; - circleButton.buttonLabel.verticallyCenterNumberLabel = YES; - } - } - - [self setNeedsLayout]; -} - -- (void)setButtonDiameter:(CGFloat)buttonDiameter -{ - if (_buttonDiameter == buttonDiameter) { return; } - _buttonDiameter = buttonDiameter; - _tappedButtonImage = nil; - _buttonImage = nil; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonSpacing:(CGSize)buttonSpacing -{ - if (CGSizeEqualToSize(_buttonSpacing, buttonSpacing)) { return; } - _buttonSpacing = buttonSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonStrokeWidth:(CGFloat)buttonStrokeWidth -{ - if (_buttonStrokeWidth== buttonStrokeWidth) { return; } - _buttonStrokeWidth = buttonStrokeWidth; - _tappedButtonImage = nil; - _buttonImage = nil; - [self updateButtonsForCurrentState]; -} - -- (void)setShowLettering:(BOOL)showLettering -{ - if (_showLettering == showLettering) { return; } - _showLettering = showLettering; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonNumberFont:(UIFont *)buttonNumberFont -{ - if (_buttonNumberFont == buttonNumberFont) { return; } - _buttonNumberFont = buttonNumberFont; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLetteringFont:(UIFont *)buttonLetteringFont -{ - if (buttonLetteringFont == _buttonLetteringFont) { return; } - _buttonLetteringFont = buttonLetteringFont; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLabelSpacing:(CGFloat)buttonLabelSpacing -{ - if (buttonLabelSpacing == _buttonLabelSpacing) { return; } - _buttonLabelSpacing = buttonLabelSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonLetteringSpacing:(CGFloat)buttonLetteringSpacing -{ - if (buttonLetteringSpacing == _buttonLetteringSpacing) { return; } - _buttonLetteringSpacing = buttonLetteringSpacing; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor -{ - if (buttonBackgroundColor == _buttonBackgroundColor) { return; } - _buttonBackgroundColor = buttonBackgroundColor; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonTextColor:(UIColor *)buttonTextColor -{ - if (buttonTextColor == _buttonTextColor) { return; } - _buttonTextColor = buttonTextColor; - [self updateButtonsForCurrentState]; -} - -- (void)setButtonHighlightedTextColor:(UIColor *)buttonHighlightedTextColor -{ - if (buttonHighlightedTextColor == _buttonHighlightedTextColor) { return; } - _buttonHighlightedTextColor = buttonHighlightedTextColor; - [self updateButtonsForCurrentState]; -} - -- (void)setLeftAccessoryView:(UIView *)leftAccessoryView -{ - if (_leftAccessoryView == leftAccessoryView) { return; } - _leftAccessoryView = leftAccessoryView; - [self addSubview:_leftAccessoryView]; - [self setNeedsLayout]; -} - -- (void)setRightAccessoryView:(UIView *)rightAccessoryView -{ - if (_rightAccessoryView == rightAccessoryView) { return; } - _rightAccessoryView = rightAccessoryView; - [self addSubview:_rightAccessoryView]; - [self setNeedsLayout]; -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - - for (TOPasscodeCircleButton *button in self.keypadButtons) { - // Skip whichever '0' button is not presently being used - if ((self.horizontalLayout && button == self.verticalZeroButton) || - (!self.horizontalLayout && button == self.horizontalZeroButton)) - { - continue; - } - - button.contentAlpha = contentAlpha; - } -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h deleted file mode 100644 index b027ee853b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.h +++ /dev/null @@ -1,136 +0,0 @@ -// -// TOPasscodeView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import "TOPasscodeViewControllerConstants.h" -#import "TOPasscodeKeypadView.h" - -NS_ASSUME_NONNULL_BEGIN - -@class TOPasscodeCircleButton; -@class TOPasscodeInputField; -@class TOPasscodeKeypadView; -@class TOPasscodeViewContentLayout; - -/** - The passcode view is the primary content view for the passcode view controller. - On iPad, every view except the background view is a subview of this view. - On iPhone, the auxiliary buttons ('Touch ID', 'Cancel') are managed by the view controller. - */ -@interface TOPasscodeView : UIView - -/* The visual style of the view */ -@property (nonatomic, assign) TOPasscodeViewStyle style; - -/* The type of passcode being managed by it */ -@property (nonatomic, readonly) TOPasscodeType passcodeType; - -/* Whether the content is laid out vertically or horizontally (iPhone only) */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/* The text in the title view (Default is 'Enter Passcode') */ -@property (nonatomic, copy) NSString *titleText; -@property (nonatomic, copy) NSString *subtitleText; - -/* Customizable Accessory Views */ -@property (nonatomic, strong, nullable) UIView *titleView; -@property (nonatomic, strong, nullable) UIButton *leftButton; -@property (nonatomic, strong, nullable) UIButton *rightButton; - -/* The default views always shown in this view */ -@property (nonatomic, readonly) UILabel *titleLabel; -@property (nonatomic, readonly) UILabel *subtitleLabel; -@property (nonatomic, readonly) TOPasscodeInputField *inputField; -@property (nonatomic, readonly) TOPasscodeKeypadView *keypadView; - -/* Overrides for theming the various elements. */ -@property (nonatomic, strong, nullable) UIColor *titleLabelColor; -@property (nonatomic, strong, nullable) UIColor *subtitleLabelColor; -@property (nonatomic, strong, nullable) UIColor *inputProgressViewTintColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonBackgroundColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonTextColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonHighlightedTextColor; - -/* Horizontal inset from edge of keypad view to button center */ -@property (nonatomic, readonly) CGFloat keypadButtonInset; - -/* An animatable property for animating the non-translucent subviews */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/* The passcode currently entered into this view */ -@property (nonatomic, copy, nullable) NSString *passcode; - -/* The default layout object controlling the - sizing and placement of all this view's child elements. */ -@property (nonatomic, strong, null_resettable) TOPasscodeViewContentLayout *defaultContentLayout; - -/* As needed, additional layout objects that will be checked and used in priority over the default content layout. */ -@property (nonatomic, strong, nullable) NSArray *contentLayouts; - -/* Callback triggered each time the user taps a key */ -@property (nonatomic, copy, nullable) void (^passcodeDigitEnteredHandler)(void); - -/* Callback triggered when the user has finished entering the passcode */ -@property (nonatomic, copy, nullable) void (^passcodeCompletedHandler)(NSString *passcode); - -/* - Create a new instance with one of the style types - - @param style The visual style of the passcode view. - @param type The type of passcode to accept. - */ -- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type; - -/* - Resize the view and all subviews for the optimum size to fit a super view of the suplied width. - - @param size The size of the view to which this view. - */ -- (void)sizeToFitSize:(CGSize)size; - -/* - Reset the passcode to nil and optionally play animation / vibration to match - - @param animated Play a shaking animation to reset the passcode. - @param impact On supported devices, play a small reset vibration as well. - */ -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; - -/* - Delete the last character from the passcode - - @param animated Whether the delete operation is animated or not. - */ -- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated; - -/* - Animate the transition between horizontal and vertical layouts - - @param horizontalLayout Whether to lay out the content vertically or horizontally. - @param animated Whether the transition is animated or not. - @param duration The duration of the animation - */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m deleted file mode 100644 index 2d1a30a3ee..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Main/TOPasscodeView.m +++ /dev/null @@ -1,690 +0,0 @@ -// -// TOPasscodeView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeView.h" -#import "TOPasscodeViewContentLayout.h" -#import "TOPasscodeCircleButton.h" -#import "TOPasscodeInputField.h" -#import "TOPasscodeKeypadView.h" - -@interface TOPasscodeView () - -/* The current layout object used to configure this view */ -@property (nonatomic, weak) TOPasscodeViewContentLayout *currentLayout; - -/* The main views */ -@property (nonatomic, strong, readwrite) UILabel *titleLabel; -@property (nonatomic, strong, readwrite) UILabel *subtitleLabel; -@property (nonatomic, strong, readwrite) TOPasscodeInputField *inputField; -@property (nonatomic, strong, readwrite) TOPasscodeKeypadView *keypadView; - -/* The type of passcode we're displaying */ -@property (nonatomic, assign, readwrite) TOPasscodeType passcodeType; - -@end - -@implementation TOPasscodeView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (instancetype)initWithStyle:(TOPasscodeViewStyle)style passcodeType:(TOPasscodeType)type -{ - if (self = [super initWithFrame:CGRectMake(0,0,320,393)]) { - _style = style; - _passcodeType = type; - [self setUp]; - } - - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)aDecoder -{ - if (self = [super initWithCoder:aDecoder]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - // Set up default properties - self.userInteractionEnabled = YES; - _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; - _currentLayout = _defaultContentLayout; - _contentLayouts = @[[TOPasscodeViewContentLayout mediumScreenContentLayout], - [TOPasscodeViewContentLayout smallScreenContentLayout]]; - _titleText = NSLocalizedString(@"Enter Passcode", @""); - - // Start configuring views - [self setUpViewForType:self.passcodeType]; - - // Set the default layout for the views - [self updateSubviewsForContentLayout:_defaultContentLayout]; - - // Configure the theme of all of the views - [self applyThemeForStyle:_style]; -} - -#pragma mark - View Layout - -- (void)verticallyLayoutSubviews -{ - CGSize viewSize = self.frame.size; - CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; - - CGRect frame = CGRectZero; - CGFloat y = 0.0f; - - // Title View - if (self.titleView) { - frame = self.titleView.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.titleView.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.titleViewBottomSpacing; - } - - // Title Label - frame = self.titleLabel.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.titleLabel.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.titleLabelBottomSpacing; - - // Circle Row View - [self.inputField sizeToFit]; - frame = self.inputField.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.inputField.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.circleRowBottomSpacing; - - // Subtitle Label - frame = self.subtitleLabel.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.subtitleLabel.frame = CGRectIntegral(frame); - - y = CGRectGetMaxY(frame) + self.currentLayout.subtitleLabelBottomSpacing; - - // PIN Pad View - if (self.keypadView) { - frame = self.keypadView.frame; - frame.origin.y = y; - frame.origin.x = midViewSize.width - (CGRectGetWidth(frame) * 0.5f); - self.keypadView.frame = CGRectIntegral(frame); - } - - // If the keypad view is hidden, lay out the left button manually - if (!self.keypadView && self.leftButton) { - frame = self.leftButton.frame; - frame.origin.x = 0.0f; - frame.origin.y = y; - self.leftButton.frame = frame; - } - - // If the keypad view is hidden, lay out the right button manually - if (!self.keypadView && self.rightButton) { - frame = self.rightButton.frame; - frame.origin.x = viewSize.width - frame.size.width; - frame.origin.y = y; - self.rightButton.frame = frame; - } -} - -- (void)horizontallyLayoutSubviews -{ - CGSize midViewSize = (CGSize){self.frame.size.width * 0.5f, self.frame.size.height * 0.5f}; - CGRect frame = CGRectZero; - - // Work out the y offset, assuming the input field is in the middle - frame.origin.y = midViewSize.height - (self.inputField.frame.size.height * 0.5f); - frame.origin.y -= (self.titleLabel.frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); - - // Include offset for title view if present - if (self.titleView) { - frame.origin.y -= (self.titleView.frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); - } - - // Set initial Y offset - frame.origin.y = MAX(frame.origin.y, 0.0f); - - // Set frame of title view - if (self.titleView) { - frame.size = self.titleView.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.titleView.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.titleViewHorizontalBottomSpacing); - } - - // Set frame of title label - frame.size = self.titleLabel.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.titleLabel.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.subtitleLabelHorizontalBottomSpacing); - - // Set frame of subtitle label - frame.size = self.subtitleLabel.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.subtitleLabel.frame = CGRectIntegral(frame); - - frame.origin.y += (frame.size.height + self.currentLayout.titleLabelHorizontalBottomSpacing); - - // Set frame of the input field - frame.size = self.inputField.frame.size; - frame.origin.x = (self.currentLayout.titleHorizontalLayoutWidth - frame.size.width) * 0.5f; - self.inputField.frame = CGRectIntegral(frame); - - // Set the frame of the keypad view - frame.size = self.keypadView.frame.size; - frame.origin.y = 0.0f; - frame.origin.x = self.currentLayout.titleHorizontalLayoutWidth + self.currentLayout.titleHorizontalLayoutSpacing; - self.keypadView.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - if (self.horizontalLayout) { - [self horizontallyLayoutSubviews]; - } - else { - [self verticallyLayoutSubviews]; - } -} - -- (void)sizeToFitSize:(CGSize)size -{ - CGFloat width = size.width; - if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { - width = MIN(size.width, size.height); - } - - NSMutableArray *layouts = [NSMutableArray array]; - [layouts addObject:self.defaultContentLayout]; - [layouts addObjectsFromArray:self.contentLayouts]; - - // Loop through each layout (in ascending order) and pick the best one to fit this view - TOPasscodeViewContentLayout *contentLayout = self.defaultContentLayout; - for (TOPasscodeViewContentLayout *layout in layouts) { - if (width >= layout.viewWidth) { - contentLayout = layout; - break; - } - } - - // Set the new layout - self.currentLayout = contentLayout; - - // Resize the views to fit - [self sizeToFit]; -} - -- (void)verticalSizeToFit -{ - CGRect frame = self.frame; - frame.size.width = 0.0f; - frame.size.height = 0.0f; - - [self.keypadView sizeToFit]; - [self.inputField sizeToFit]; - - if (self.keypadView) { - frame.size.width = self.keypadView.frame.size.width; - } - else { - frame.size.width = self.inputField.frame.size.width; - } - - // Add height for the title view - if (self.titleView) { - frame.size.height += self.titleView.frame.size.height; - frame.size.height += self.currentLayout.titleViewBottomSpacing; - } - - // Add height for the title label - CGRect titleFrame = self.titleLabel.frame; - titleFrame.size = [self.titleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; - self.titleLabel.frame = titleFrame; - - frame.size.height += titleFrame.size.height; - frame.size.height += self.currentLayout.titleLabelBottomSpacing; - - // Add height for the subtitle label - CGRect subtitleFrame = self.subtitleLabel.frame; - subtitleFrame.size = [self.subtitleLabel sizeThatFits:(CGSize){frame.size.width, CGFLOAT_MAX}]; - self.subtitleLabel.frame = subtitleFrame; - - frame.size.height += subtitleFrame.size.height; - frame.size.height += self.currentLayout.subtitleLabelBottomSpacing; - - // Add height for the circle rows - frame.size.height += self.inputField.frame.size.height; - frame.size.height += self.currentLayout.circleRowBottomSpacing; - - // Add height for the keypad - if (self.keypadView) { - frame.size.height += self.keypadView.frame.size.height; - } - else { // If no keypad, just factor in the accessory buttons - [self.leftButton sizeToFit]; - [self.rightButton sizeToFit]; - - CGFloat maxHeight = 0.0f; - maxHeight = MAX(self.leftButton.frame.size.height, 0.0f); - maxHeight = MAX(self.rightButton.frame.size.height, maxHeight); - - frame.size.height += maxHeight; - } - - // Add extra padding at the bottom - frame.size.height += self.currentLayout.bottomPadding; - - // Set the frame back - self.frame = CGRectIntegral(frame); -} - -- (void)horizontalSizeToFit -{ - CGRect frame = self.frame; - - [self.keypadView sizeToFit]; - [self.inputField sizeToFit]; - - frame.size.width = self.currentLayout.titleHorizontalLayoutWidth; - frame.size.width += self.currentLayout.titleHorizontalLayoutSpacing; - frame.size.width += self.keypadView.frame.size.width; - - frame.size.height = self.keypadView.frame.size.height; - - self.frame = CGRectIntegral(frame); -} - -- (void)sizeToFit -{ - if (self.horizontalLayout && self.passcodeType != TOPasscodeTypeCustomAlphanumeric) { - [self horizontalSizeToFit]; - } - else { - [self verticalSizeToFit]; - } -} - -#pragma mark - View Setup - -- (void)setUpViewForType:(TOPasscodeType)type -{ - __weak typeof(self) weakSelf = self; - - self.backgroundColor = [UIColor clearColor]; - - // Set up title label - if (self.titleLabel == nil) { - self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - } - self.titleLabel.text = self.titleText; - self.titleLabel.textAlignment = NSTextAlignmentCenter; - self.titleLabel.numberOfLines = 0; - [self.titleLabel sizeToFit]; - [self addSubview:self.titleLabel]; - - // Set up the passcode style - TOPasscodeInputFieldStyle style = TOPasscodeInputFieldStyleFixed; - if (type >= TOPasscodeTypeCustomNumeric) { - style = TOPasscodeInputFieldStyleVariable; - } - - // Set up input field - if (self.inputField == nil) { - self.inputField = [[TOPasscodeInputField alloc] initWithStyle:style]; - } - self.inputField.passcodeCompletedHandler = ^(NSString *passcode) { - if (weakSelf.passcodeCompletedHandler) { - weakSelf.passcodeCompletedHandler(passcode); - } - }; - - // Configure the input field based on the exact passcode type - if (style == TOPasscodeInputFieldStyleFixed) { - self.inputField.fixedInputView.length = (self.passcodeType == TOPasscodeTypeSixDigits) ? 6 : 4; - } - else { - self.inputField.showSubmitButton = (self.passcodeType == TOPasscodeTypeCustomNumeric); - self.inputField.enabled = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric); - } - - [self addSubview:self.inputField]; - - // Set up subtitle label - if (self.subtitleLabel == nil) { - self.subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - } - self.subtitleLabel.text = self.subtitleText; - self.subtitleLabel.textAlignment = NSTextAlignmentCenter; - self.subtitleLabel.numberOfLines = 0; - [self.subtitleLabel sizeToFit]; - [self addSubview:self.subtitleLabel]; - - // Set up pad row - if (type != TOPasscodeTypeCustomAlphanumeric) { - if (self.keypadView == nil) { - self.keypadView = [[TOPasscodeKeypadView alloc] init]; - } - self.keypadView.buttonTappedHandler = ^(NSInteger button) { - NSString *numberString = [NSString stringWithFormat:@"%ld", (long)button]; - [weakSelf.inputField appendPasscodeCharacters:numberString animated:NO]; - - if (weakSelf.passcodeDigitEnteredHandler) { - weakSelf.passcodeDigitEnteredHandler(); - } - }; - [self addSubview:self.keypadView]; - } - else { - [self.keypadView removeFromSuperview]; - self.keypadView = nil; - } -} - -- (void)updateSubviewsForContentLayout:(TOPasscodeViewContentLayout *)contentLayout -{ - // Title View - self.titleLabel.font = contentLayout.titleLabelFont; - - // Subtitle View - self.subtitleLabel.font = contentLayout.subtitleLabelFont; - - // Circle Row View - self.inputField.fixedInputView.circleDiameter = contentLayout.circleRowDiameter; - self.inputField.fixedInputView.circleSpacing = contentLayout.circleRowSpacing; - - // Text Field Input Row - NSInteger maximumInputLength = (self.passcodeType == TOPasscodeTypeCustomAlphanumeric) ? - contentLayout.textFieldAlphanumericCharacterLength : - contentLayout.textFieldNumericCharacterLength; - - self.inputField.variableInputView.outlineThickness = contentLayout.textFieldBorderThickness; - self.inputField.variableInputView.outlineCornerRadius = contentLayout.textFieldBorderRadius; - self.inputField.variableInputView.circleDiameter = contentLayout.textFieldCircleDiameter; - self.inputField.variableInputView.circleSpacing = contentLayout.textFieldCircleSpacing; - self.inputField.variableInputView.outlinePadding = contentLayout.textFieldBorderPadding; - self.inputField.variableInputView.maximumVisibleLength = maximumInputLength; - - // Submit button - self.inputField.submitButtonSpacing = contentLayout.submitButtonSpacing; - self.inputField.submitButtonFontSize = contentLayout.submitButtonFontSize; - - // Keypad - self.keypadView.buttonNumberFont = contentLayout.circleButtonTitleLabelFont; - self.keypadView.buttonLetteringFont = contentLayout.circleButtonLetteringLabelFont; - self.keypadView.buttonLetteringSpacing = contentLayout.circleButtonLetteringSpacing; - self.keypadView.buttonLabelSpacing = contentLayout.circleButtonLabelSpacing; - self.keypadView.buttonSpacing = contentLayout.circleButtonSpacing; - self.keypadView.buttonDiameter = contentLayout.circleButtonDiameter; -} - -- (void)applyThemeForStyle:(TOPasscodeViewStyle)style -{ - BOOL isTranslucent = TOPasscodeViewStyleIsTranslucent(style); - BOOL isDark = TOPasscodeViewStyleIsDark(style); - - // Set title label color - UIColor *titleLabelColor = self.titleLabelColor; - if (titleLabelColor == nil) { - titleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.titleLabel.textColor = titleLabelColor; - - // Set subtitle label color - UIColor *subtitleLabelColor = self.subtitleLabelColor; - if (subtitleLabelColor == nil) { - subtitleLabelColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.subtitleLabel.textColor = subtitleLabelColor; - - // Add/remove the translucency effect to the buttons - if (isTranslucent) { - UIBlurEffect *blurEffect = [self blurEffectForStyle:style]; - UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect]; - self.inputField.visualEffectView.effect = vibrancyEffect; - self.keypadView.vibrancyEffect = vibrancyEffect; - } - else { - self.inputField.visualEffectView.effect = nil; - self.keypadView.vibrancyEffect = nil; - } - - // Set keyboard style of the input field - self.inputField.keyboardAppearance = isDark ? UIKeyboardAppearanceDark : UIKeyboardAppearanceDefault; - - UIColor *defaultTintColor = isDark ? [UIColor colorWithWhite:0.85 alpha:1.0f] : [UIColor colorWithWhite:0.3 alpha:1.0f]; - - // Set the tint color of the circle row view - UIColor *circleRowColor = self.inputProgressViewTintColor; - if (circleRowColor == nil) { - circleRowColor = defaultTintColor; - } - self.inputField.tintColor = defaultTintColor; - - // Set the tint color of the keypad buttons - UIColor *keypadButtonBackgroundColor = self.keypadButtonBackgroundColor; - if (keypadButtonBackgroundColor == nil) { - keypadButtonBackgroundColor = defaultTintColor; - } - self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; - - // Set the color of the keypad button labels - UIColor *buttonTextColor = self.keypadButtonTextColor; - if (buttonTextColor == nil) { - buttonTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - } - self.keypadView.buttonTextColor = buttonTextColor; - - // Set the highlight color of the keypad button - UIColor *buttonHighlightedTextColor = self.keypadButtonHighlightedTextColor; - if (buttonHighlightedTextColor == nil) { - if (isTranslucent) { - buttonHighlightedTextColor = isDark ? nil : [UIColor whiteColor]; - } - else { - buttonHighlightedTextColor = isDark ? [UIColor blackColor] : [UIColor whiteColor]; - } - } - self.keypadView.buttonHighlightedTextColor = buttonHighlightedTextColor; -} - -#pragma mark - Passcode Management - -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact -{ - [self.inputField resetPasscodeAnimated:animated playImpact:impact]; -} - -- (void)deleteLastPasscodeCharacterAnimated:(BOOL)animated -{ - [self.inputField deletePasscodeCharactersOfCount:1 animated:animated]; -} - -#pragma mark - Internal Style Management - -- (UIBlurEffect *)blurEffectForStyle:(TOPasscodeViewStyle)style -{ - switch (style) { - case TOPasscodeViewStyleTranslucentDark: - return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; - case TOPasscodeViewStyleTranslucentLight: - return [UIBlurEffect effectWithStyle:UIBlurEffectStyleExtraLight]; - default: return nil; - } - - return nil; -} - -#pragma mark - Accessors - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (horizontalLayout == _horizontalLayout) { return; } - _horizontalLayout = horizontalLayout; - [self.keypadView setHorizontalLayout:horizontalLayout animated:animated duration:duration]; - [self.inputField setHorizontalLayout:horizontalLayout animated:animated duration:duration]; -} - -- (void)setDefaultContentLayout:(TOPasscodeViewContentLayout *)defaultContentLayout -{ - if (defaultContentLayout == _defaultContentLayout) { return; } - _defaultContentLayout = defaultContentLayout; - - if (!_defaultContentLayout) { - _defaultContentLayout = [TOPasscodeViewContentLayout defaultScreenContentLayout]; - } -} - -- (void)setCurrentLayout:(TOPasscodeViewContentLayout *)currentLayout -{ - if (_currentLayout == currentLayout) { return; } - _currentLayout = currentLayout; - - // Update the views - [self updateSubviewsForContentLayout:currentLayout]; -} - -- (void)setStyle:(TOPasscodeViewStyle)style -{ - if (style == _style) { return; } - _style = style; - [self applyThemeForStyle:style]; -} - -- (void)setTitleLabelColor:(UIColor *)titleLabelColor -{ - if (titleLabelColor == _titleLabelColor) { return; } - _titleLabelColor = titleLabelColor; - self.titleLabel.textColor = titleLabelColor; -} - -- (void)setSubtitleLabelColor:(UIColor *)subtitleLabelColor -{ - if (subtitleLabelColor == _subtitleLabelColor) { return; } - _subtitleLabelColor = subtitleLabelColor; - self.subtitleLabel.textColor = subtitleLabelColor; -} - -- (void)setInputProgressViewTintColor:(UIColor *)inputProgressViewTintColor -{ - if (inputProgressViewTintColor == _inputProgressViewTintColor) { return; } - _inputProgressViewTintColor = inputProgressViewTintColor; - self.inputField.tintColor = inputProgressViewTintColor; -} - -- (void)setKeypadButtonBackgroundColor:(UIColor *)keypadButtonBackgroundColor -{ - if (keypadButtonBackgroundColor == _keypadButtonBackgroundColor) { return; } - _keypadButtonBackgroundColor = keypadButtonBackgroundColor; - self.keypadView.buttonBackgroundColor = keypadButtonBackgroundColor; -} - -- (void)setKeypadButtonTextColor:(UIColor *)keypadButtonTextColor -{ - if (keypadButtonTextColor == _keypadButtonTextColor) { return; } - _keypadButtonTextColor = keypadButtonTextColor; - self.keypadView.buttonTextColor = keypadButtonTextColor; -} - -- (void)setKeypadButtonHighlightedTextColor:(UIColor *)keypadButtonHighlightedTextColor -{ - if (keypadButtonHighlightedTextColor == _keypadButtonHighlightedTextColor) { return; } - _keypadButtonHighlightedTextColor = keypadButtonHighlightedTextColor; - self.keypadView.buttonHighlightedTextColor = keypadButtonHighlightedTextColor; -} - -- (void)setLeftButton:(UIButton *)leftButton -{ - if (leftButton == _leftButton) { return; } - _leftButton = leftButton; - - if (self.keypadView) { - self.keypadView.leftAccessoryView = leftButton; - } - else { - [self addSubview:_leftButton]; - } -} - -- (void)setRightButton:(UIButton *)rightButton -{ - if (rightButton == _rightButton) { return; } - _rightButton = rightButton; - - if (self.keypadView) { - self.keypadView.rightAccessoryView = rightButton; - } - else { - [self addSubview:_rightButton]; - } -} - -- (CGFloat)keypadButtonInset -{ - UIView *button = self.keypadView.keypadButtons.firstObject; - return CGRectGetMidX(button.frame); -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - - self.titleView.alpha = contentAlpha; - self.titleLabel.alpha = contentAlpha; - self.subtitleLabel.alpha = contentAlpha; - self.inputField.contentAlpha = contentAlpha; - self.keypadView.contentAlpha = contentAlpha; - self.keypadView.leftAccessoryView.alpha = contentAlpha; - self.keypadView.rightAccessoryView.alpha = contentAlpha; - self.leftButton.alpha = contentAlpha; - self.rightButton.alpha = contentAlpha; -} - -- (void)setPasscode:(NSString *)passcode -{ - [self.inputField setPasscode:passcode]; -} - -- (NSString *)passcode -{ - return self.inputField.passcode; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h deleted file mode 100644 index 8179622f61..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// TOPasscodeSettingsKeypadButton.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -@class TOPasscodeButtonLabel; - -/** - A single button view that is used in a number keypad views styled in - a pseudo-skeuomorphic style. - */ -@interface TOPasscodeSettingsKeypadButton : UIButton - -/** Background Images */ -@property (nonatomic, strong) UIImage *buttonBackgroundImage; -@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; - -/* Inset of the label view from the bottom to account for the bevel */ -@property (nonatomic, assign) CGFloat bottomInset; - -/* The button label containing the number and lettering */ -@property (nonatomic, readonly) TOPasscodeButtonLabel *buttonLabel; - -+ (TOPasscodeSettingsKeypadButton *)button; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m deleted file mode 100644 index fa41bd4587..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadButton.m +++ /dev/null @@ -1,103 +0,0 @@ -// -// TOPasscodeSettingsKeypadButton.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeSettingsKeypadButton.h" -#import "TOPasscodeButtonLabel.h" - -@interface TOPasscodeSettingsKeypadButton () - -@property (nonatomic, strong, readwrite) TOPasscodeButtonLabel *buttonLabel; - -@end - -@implementation TOPasscodeSettingsKeypadButton - -+ (TOPasscodeSettingsKeypadButton *)button -{ - TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton buttonWithType:UIButtonTypeCustom]; - button.frame = CGRectMake(0,0,100,60); - return button; -} - -#pragma mark - Lazy Accessor - -- (TOPasscodeButtonLabel *)buttonLabel -{ - if (_buttonLabel) { return _buttonLabel; } - - CGRect frame = self.bounds; - frame.size.height -= self.bottomInset; - - _buttonLabel = [[TOPasscodeButtonLabel alloc] initWithFrame:frame]; - _buttonLabel.userInteractionEnabled = NO; - _buttonLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self addSubview:_buttonLabel]; - - return _buttonLabel; -} - -#pragma mark - Layout Accessor - -- (void)setBottomInset:(CGFloat)bottomInset -{ - _bottomInset = bottomInset; - - CGRect frame = self.bounds; - frame.size.height -= _bottomInset; - self.buttonLabel.frame = frame; - [self setNeedsLayout]; -} - -#pragma mark - Control Accessor - -- (void)setEnabled:(BOOL)enabled -{ - [super setEnabled:enabled]; - self.buttonLabel.alpha = enabled ? 1.0f : 0.5f; -} - -#pragma mark - Background Image Accessor - - -- (void)setHighlighted:(BOOL)highlighted { - [self.layer removeAllAnimations]; - [UIView transitionWithView:self - duration:0.25 - options:UIViewAnimationOptionTransitionCrossDissolve | - UIViewAnimationOptionAllowAnimatedContent | - UIViewAnimationOptionAllowUserInteraction - animations:^{ - [super setHighlighted:highlighted]; - } completion:nil]; -} - -- (void)setButtonBackgroundImage:(UIImage *)buttonBackgroundImage -{ - [self setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal]; -} - -- (UIImage *)buttonBackgroundImage { return [self backgroundImageForState:UIControlStateNormal]; } - -- (void)setButtonTappedBackgroundImage:(UIImage *)buttonTappedBackgroundImage -{ - [self setBackgroundImage:buttonTappedBackgroundImage forState:UIControlStateHighlighted]; -} - -- (UIImage *)buttonTappedBackgroundImage { return [self backgroundImageForState:UIControlStateHighlighted]; } - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h deleted file mode 100644 index 13234bd07b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.h +++ /dev/null @@ -1,74 +0,0 @@ -// -// TOPasscodeSettingsKeypadView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 -#import "TOPasscodeViewControllerConstants.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - A keypad view of 9 buttons that allow numerical input on both iPad and iPhone. - Designed to match the base system, with a pseudo-skeuomorphical styling. - */ -@interface TOPasscodeSettingsKeypadView : UIView - -/* Whether the control is allowing input */ -@property (nonatomic, assign) BOOL enabled; - -/* Whether the view is currently light mode or dark. */ -@property (nonatomic, assign) TOPasscodeSettingsViewStyle style; - -/* The color of the separator line */ -@property (nonatomic, strong) UIColor *separatorLineColor; - -/* Labels in the buttons are laid out horizontally */ -@property (nonatomic, assign) BOOL buttonLabelHorizontalLayout; - -/* If overridden, the foreground color of the buttons */ -@property (nonatomic, assign) CGFloat keypadButtonBorderThickness; - -/* Untapped background images */ -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonForegroundColor; -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonBorderColor; - -/* Tapped background images */ -@property (nonatomic, strong, null_resettable) UIColor *keypadButtonTappedForegroundColor; -@property (nonatomic, strong, nullable) UIColor *keypadButtonTappedBorderColor; - -/* Button label styling */ -@property (nonatomic, strong) UIFont *keypadButtonNumberFont; -@property (nonatomic, strong) UIFont *keypadButtonLetteringFont; -@property (nonatomic, strong) UIColor *keypadButtonLabelTextColor; -@property (nonatomic, assign) CGFloat keypadButtonVerticalSpacing; -@property (nonatomic, assign) CGFloat keypadButtonHorizontalSpacing; -@property (nonatomic, assign) CGFloat keypadButtonLetteringSpacing; - -/* Callback handlers */ -@property (nonatomic, copy) void (^numberButtonTappedHandler)(NSInteger number); -@property (nonatomic, copy) void (^deleteButtonTappedHandler)(void); - -/* In really small sizes, set the keypad labels to horizontal */ -- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m deleted file mode 100644 index eda81bb61b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsKeypadView.m +++ /dev/null @@ -1,395 +0,0 @@ -// -// TOPasscodeSettingsKeypadView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeSettingsKeypadView.h" -#import "TOPasscodeSettingsKeypadButton.h" -#import "TOPasscodeButtonLabel.h" -#import "TOSettingsKeypadImage.h" - -const CGFloat kTOPasscodeSettingsKeypadButtonInnerSpacing = 7.0f; -const CGFloat kTOPasscodeSettingsKeypadButtonOuterSpacing = 7.0f; -const CGFloat kTOPasscodeSettingsKeypadCornderRadius = 12.0f; - -@interface TOPasscodeSettingsKeypadView () - -@property (nonatomic, strong) UIView *separatorView; -@property (nonatomic, strong) NSArray *keypadButtons; -@property (nonatomic, strong) UIButton *deleteButton; - -@property (nonatomic, strong) UIImage *buttonBackgroundImage; -@property (nonatomic, strong) UIImage *buttonTappedBackgroundImage; - -@end - -@implementation TOPasscodeSettingsKeypadView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - /* Button label styling */ - _keypadButtonNumberFont = [UIFont systemFontOfSize:32.0f weight:UIFontWeightRegular]; - _keypadButtonLetteringFont = [UIFont systemFontOfSize:11.0f weight:UIFontWeightRegular]; - _keypadButtonVerticalSpacing = 2.0f; - _keypadButtonHorizontalSpacing = 3.0f; - _keypadButtonLetteringSpacing = 2.0f; - - CGSize viewSize = self.frame.size; - CGFloat height = 1.0f / [[UIScreen mainScreen] scale]; - self.separatorView = [[UIView alloc] initWithFrame:(CGRect){CGPointZero,{viewSize.width, height}}]; - self.separatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth; - [self addSubview:self.separatorView]; - - [self setUpKeypadButtons]; - [self setUpDeleteButton]; - - [self setUpDefaultValuesForStye:_style]; - [self applyTheme]; -} - -- (void)setUpKeypadButtons -{ - NSInteger numberOfButtons = 10; - NSArray *letteredTitles = @[@"ABC", @"DEF", @"GHI", @"JKL", - @"MNO", @"PQRS", @"TUV", @"WXYZ"]; - - NSMutableArray *buttons = [NSMutableArray arrayWithCapacity:10]; - for (NSInteger i = 0; i < numberOfButtons; i++) { - NSInteger number = (i+1) % 10; // Wrap around 0 at the end - TOPasscodeSettingsKeypadButton *button = [TOPasscodeSettingsKeypadButton button]; - button.buttonLabel.numberString = [NSString stringWithFormat:@"%ld", (long)number]; - button.bottomInset = 2.0f; - button.tag = number; - - if (i > 0) { - NSInteger j = i - 1; - if (j < letteredTitles.count) { - button.buttonLabel.letteringString = letteredTitles[j]; - } - } - - [button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchDown]; - - [self addSubview:button]; - [buttons addObject:button]; - } - - self.keypadButtons = [NSArray arrayWithArray:buttons]; -} - -- (void)setUpDeleteButton -{ - UIImage *deleteIcon = [TOSettingsKeypadImage deleteIcon]; - self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.deleteButton setImage:deleteIcon forState:UIControlStateNormal]; - self.deleteButton.contentMode = UIViewContentModeCenter; - self.deleteButton.frame = (CGRect){CGPointZero, deleteIcon.size}; - self.deleteButton.tintColor = [UIColor blackColor]; - [self.deleteButton addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self addSubview:self.deleteButton]; -} - -- (void)setUpDefaultValuesForStye:(TOPasscodeSettingsViewStyle)style -{ - BOOL isDark = style == TOPasscodeSettingsViewStyleDark; - - // Keypad label - self.keypadButtonLabelTextColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; - - self.keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.35f alpha:1.0f] : [UIColor whiteColor]; - self.keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.45f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; - - // Button border color - UIColor *borderColor = nil; - if (isDark) { - borderColor = [UIColor colorWithWhite:0.15f alpha:1.0f]; - } - else { - borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; - } - self.keypadButtonBorderColor = borderColor; - - // Background Color - UIColor *backgroundColor = nil; - if (isDark) { - backgroundColor = [UIColor colorWithWhite:0.18f alpha:1.0f]; - } - else { - backgroundColor = [UIColor colorWithRed:220.0f/255.0f green:225.0f/255.0f blue:232.0f/255.0f alpha:1.0f]; - } - self.backgroundColor = backgroundColor; - - // Separator lines - UIColor *separatorColor = nil; - if (isDark) { - separatorColor = [UIColor colorWithWhite:0.25f alpha:1.0f]; - } - else { - separatorColor = [UIColor colorWithWhite:0.7f alpha:1.0f]; - } - self.separatorView.backgroundColor = separatorColor; - - self.deleteButton.tintColor = isDark ? [UIColor whiteColor] : [UIColor blackColor]; -} - -- (void)setUpImagesIfNeeded -{ - if (self.buttonBackgroundImage && self.buttonTappedBackgroundImage) { - return; - } - - if (self.buttonBackgroundImage == nil) { - self.buttonBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius - foregroundColor:self.keypadButtonForegroundColor - edgeColor:self.keypadButtonBorderColor - edgeThickness:2.0f]; - } - - if (self.buttonTappedBackgroundImage == nil) { - self.buttonTappedBackgroundImage = [TOSettingsKeypadImage buttonImageWithCornerRadius:kTOPasscodeSettingsKeypadCornderRadius - foregroundColor:self.keypadButtonTappedForegroundColor - edgeColor:self.keypadButtonBorderColor - edgeThickness:2.0f]; - } - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.buttonBackgroundImage = self.buttonBackgroundImage; - button.buttonTappedBackgroundImage = self.buttonTappedBackgroundImage; - } -} - -- (void)applyTheme -{ - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.buttonLabel.textColor = self.keypadButtonLabelTextColor; - button.buttonLabel.letteringCharacterSpacing = self.keypadButtonLetteringSpacing; - button.buttonLabel.letteringVerticalSpacing = self.keypadButtonVerticalSpacing; - button.buttonLabel.letteringHorizontalSpacing = self.keypadButtonHorizontalSpacing; - button.buttonLabel.numberLabelFont = self.keypadButtonNumberFont; - button.buttonLabel.letteringLabelFont = self.keypadButtonLetteringFont; - } -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - [self setUpImagesIfNeeded]; - - CGFloat outerSpacing = kTOPasscodeSettingsKeypadButtonOuterSpacing; - CGFloat innerSpacing = kTOPasscodeSettingsKeypadButtonInnerSpacing; - - CGSize viewSize = self.bounds.size; - CGSize buttonSize = CGSizeZero; - - viewSize.width -= (outerSpacing * 2.0f); - viewSize.height -= (outerSpacing * 2.0f); - - // Pull the buttons up to avoid overlapping the home indicator on iPhone X - if (@available(iOS 11.0, *)) { - viewSize.height -= self.safeAreaInsets.bottom; - } - - // Four rows of three buttons - buttonSize.width = floorf((viewSize.width - (innerSpacing * 2.0f)) / 3.0f); - buttonSize.height = floorf((viewSize.height - (innerSpacing * 3.0f)) / 4.0f); - - CGPoint point = CGPointMake(outerSpacing, outerSpacing); - CGRect buttonFrame = (CGRect){point, buttonSize}; - - NSInteger i = 0; - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.frame = buttonFrame; - buttonFrame.origin.x += buttonFrame.size.width + innerSpacing; - - if (++i % 3 == 0) { - buttonFrame.origin.x = outerSpacing; - buttonFrame.origin.y += buttonFrame.size.height + innerSpacing; - } - - if (button == self.keypadButtons.lastObject) { - button.frame = buttonFrame; - } - } - - //Layout delete button - CGSize boundsSize = self.bounds.size; - - // Adjust for home indicator on iPhone X - if (@available(iOS 11.0, *)) { - boundsSize.height -= self.safeAreaInsets.bottom; - } - - CGRect frame = self.deleteButton.frame; - frame.size = buttonSize; - frame.origin.x = boundsSize.width - (outerSpacing + buttonSize.width * 0.5f); - frame.origin.x -= (CGRectGetWidth(frame) * 0.5f); - frame.origin.y = boundsSize.height - (outerSpacing + buttonSize.height * 0.5f); - frame.origin.y -= (CGRectGetHeight(frame) * 0.5f); - self.deleteButton.frame = frame; -} - -#pragma mark - Interaction - -- (void)buttonTapped:(id)sender -{ - // Handler for the delete button - if (sender == self.deleteButton) { - if (self.deleteButtonTappedHandler) { - self.deleteButtonTappedHandler(); - } - return; - } - - // Handler for the keypad buttons - UIButton *button = (UIButton *)sender; - NSInteger number = button.tag; - - [[UIDevice currentDevice] playInputClick]; - - if (self.numberButtonTappedHandler) { - self.numberButtonTappedHandler(number); - } -} - -#pragma mark - Accessors - - -- (void)setStyle:(TOPasscodeSettingsViewStyle)style -{ - if (style == _style) { - return; - } - - _style = style; - [self setUpDefaultValuesForStye:_style]; - [self applyTheme]; -} - -#pragma mark - Label Layout - -- (void)setButtonLabelHorizontalLayout:(BOOL)buttonLabelHorizontalLayout -{ - [self setButtonLabelHorizontalLayout:buttonLabelHorizontalLayout animated:NO]; -} - -- (void)setButtonLabelHorizontalLayout:(BOOL)horizontal animated:(BOOL)animated -{ - if (horizontal == _buttonLabelHorizontalLayout) { return; } - - _buttonLabelHorizontalLayout = horizontal; - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - if (!animated) { - button.buttonLabel.horizontalLayout = horizontal; - continue; - } - - UIView *snapshotView = [button.buttonLabel snapshotViewAfterScreenUpdates:NO]; - snapshotView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin; - [button addSubview:snapshotView]; - - button.buttonLabel.horizontalLayout = horizontal; - [button.buttonLabel setNeedsLayout]; - [button.buttonLabel layoutIfNeeded]; - - [button.buttonLabel.layer removeAllAnimations]; - for (CALayer *sublayer in button.buttonLabel.layer.sublayers) { - [sublayer removeAllAnimations]; - } - - button.buttonLabel.alpha = 0.0f; - [UIView animateWithDuration:0.4f animations:^{ - button.buttonLabel.alpha = 1.0f; - snapshotView.alpha = 0.0f; - snapshotView.center = button.buttonLabel.center; - } completion:^(BOOL complete) { - [snapshotView removeFromSuperview]; - }]; - } -} - -#pragma mark - Null Resettable Accessors - -- (void)setKeypadButtonForegroundColor:(nullable UIColor *)keypadButtonForegroundColor -{ - if (keypadButtonForegroundColor == _keypadButtonForegroundColor) { return; } - _keypadButtonForegroundColor = keypadButtonForegroundColor; - - if (_keypadButtonForegroundColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - _keypadButtonForegroundColor = isDark ? [UIColor colorWithWhite:0.3f alpha:1.0f] : [UIColor whiteColor]; - } - - self.buttonBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setKeypadButtonBorderColor:(nullable UIColor *)keypadButtonBorderColor -{ - if (keypadButtonBorderColor == _keypadButtonBorderColor) { return; } - _keypadButtonBorderColor = keypadButtonBorderColor; - - if (_keypadButtonBorderColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - UIColor *borderColor = nil; - if (isDark) { - borderColor = [UIColor colorWithWhite:0.2 alpha:1.0f]; - } - else { - borderColor = [UIColor colorWithRed:166.0f/255.0f green:174.0f/255.0f blue:186.0f/255.0f alpha:1.0f]; - } - _keypadButtonBorderColor = borderColor; - } - - self.buttonBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setKeypadButtonTappedForegroundColor:(nullable UIColor *)keypadButtonTappedForegroundColor -{ - if (keypadButtonTappedForegroundColor == _keypadButtonTappedForegroundColor) { return; } - _keypadButtonTappedForegroundColor = keypadButtonTappedForegroundColor; - - if (_keypadButtonTappedForegroundColor == nil) { - BOOL isDark = self.style == TOPasscodeSettingsViewStyleDark; - _keypadButtonTappedForegroundColor = isDark ? [UIColor colorWithWhite:0.4f alpha:1.0f] : [UIColor colorWithWhite:0.85f alpha:1.0f]; - } - - self.buttonTappedBackgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setEnabled:(BOOL)enabled -{ - _enabled = enabled; - - for (TOPasscodeSettingsKeypadButton *button in self.keypadButtons) { - button.enabled = enabled; - } - - self.deleteButton.enabled = enabled; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h deleted file mode 100644 index 5455a70586..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.h +++ /dev/null @@ -1,43 +0,0 @@ -// -// TOPasscodeSettingsWarningLabel.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -/** - When a user enters an incorrect passcode in the settings interface, - this view is displayed to show the number of failed attempts. - */ -@interface TOPasscodeSettingsWarningLabel : UIImageView - -/** The number of incorrect passcode attempts to display */ -@property (nonatomic, assign) NSInteger numberOfWarnings; - -/** The font of the text */ -@property (nonatomic, strong) UIFont *textFont; - -/** The background color of the view */ -@property (nonatomic, strong) UIColor *backgroundColor; - -/** Set the padding around the label */ -@property (nonatomic, assign) CGSize textPadding; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m deleted file mode 100644 index 27aa470507..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Settings/TOPasscodeSettingsWarningLabel.m +++ /dev/null @@ -1,154 +0,0 @@ -// -// TOPasscodeSettingsWarningLabel.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeSettingsWarningLabel.h" - -@interface TOPasscodeSettingsWarningLabel () -@property (nonatomic, strong) UILabel *label; -@end - -@implementation TOPasscodeSettingsWarningLabel - -@synthesize backgroundColor = __backgroundColor; - -#pragma mark - View Setup - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - } - - return self; -} - -- (void)setUp -{ - _numberOfWarnings = 0; - _textPadding = CGSizeMake(14.0f, 6.0f); - - self.tintColor = [UIColor colorWithRed:214.0f/255.0f green:63.0f/255.0f blue:63.0f/255.0f alpha:1.0f]; - - self.label = [[UILabel alloc] initWithFrame:CGRectZero]; - self.label.backgroundColor = [UIColor clearColor]; - self.label.textAlignment = NSTextAlignmentCenter; - self.label.textColor = [UIColor whiteColor]; - self.label.font = [UIFont systemFontOfSize:15.0f]; - [self setTextForCount:0]; - [self.label sizeToFit]; - [self addSubview:self.label]; -} - -- (void)didMoveToSuperview -{ - [super didMoveToSuperview]; - [self setBackgroundImageIfNeeded]; -} - -#pragma mark - View Layout - - -- (void)sizeToFit -{ - [super sizeToFit]; - [self.label sizeToFit]; - - CGRect labelFrame = self.label.frame; - CGRect frame = self.frame; - - labelFrame = CGRectInset(labelFrame, -self.textPadding.width, -self.textPadding.height); - frame.size = labelFrame.size; - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - CGRect frame = self.frame; - CGRect labelFrame = self.label.frame; - - labelFrame.origin.x = (CGRectGetWidth(frame) - CGRectGetWidth(labelFrame)) * 0.5f; - labelFrame.origin.y = (CGRectGetHeight(frame) - CGRectGetHeight(labelFrame)) * 0.5f; - self.label.frame = labelFrame; -} - -#pragma mark - View State Handling - - -- (void)setTextForCount:(NSInteger)count -{ - NSString *text = nil; - if (count == 1) { - text = NSLocalizedString(@"1 Failed Passcode Attempt", @""); - } - else { - text = [NSString stringWithFormat:NSLocalizedString(@"%ld Failed Passcode Attempts", @""), count]; - } - self.label.text = text; - - [self sizeToFit]; -} - -#pragma mark - Background Image Managements - - -- (void)setBackgroundImageIfNeeded -{ - // Don't bother if we're not in a view - if (self.superview == nil) { return; } - - // Compare the view height and don't proceed if - if (lround(self.image.size.height) == lround(self.frame.size.height)) { return; } - - // Create the image - self.image = [[self class] roundedBackgroundImageWithHeight:self.frame.size.height]; -} - -+ (UIImage *)roundedBackgroundImageWithHeight:(CGFloat)height -{ - UIImage *image = nil; - CGRect frame = CGRectZero; - frame.size.width = height + 1.0; - frame.size.height = height; - - UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0f); - { - UIBezierPath* path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:height * 0.5f]; - [[UIColor blackColor] setFill]; - [path fill]; - - image = UIGraphicsGetImageFromCurrentImageContext(); - } - UIGraphicsEndImageContext(); - - CGFloat halfHeight = height * 0.5f; - UIEdgeInsets insets = UIEdgeInsetsMake(halfHeight, halfHeight, halfHeight, halfHeight); - image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; - image = [image resizableImageWithCapInsets:insets]; - return image; -} - -#pragma mark - Accessors - - -- (void)setNumberOfWarnings:(NSInteger)numberOfWarnings -{ - _numberOfWarnings = numberOfWarnings; - [self setTextForCount:_numberOfWarnings]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h deleted file mode 100644 index 12b4d00fef..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.h +++ /dev/null @@ -1,61 +0,0 @@ -// -// TOPasscodeButtonLabel.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -NS_ASSUME_NONNULL_BEGIN - -/** - A view that manages two label subviews: a larger label showing a single number - and a smaller label showing lettering as well. - */ -@interface TOPasscodeButtonLabel : UIView - -// Draws the lettering label to the side -@property (nonatomic, assign) BOOL horizontalLayout; - -// The strings of both labels -@property (nonatomic, copy) NSString *numberString; -@property (nonatomic, copy, nullable) NSString *letteringString; - -// The color of both labels -@property (nonatomic, strong) UIColor *textColor; - -// The label views -@property (nonatomic, readonly) UILabel *numberLabel; -@property (nonatomic, readonly) UILabel *letteringLabel; - -// The fonts for each label (In case they are nil) -@property (nonatomic, strong) UIFont *numberLabelFont; -@property (nonatomic, strong) UIFont *letteringLabelFont; - -// Has initial default values -@property (nonatomic, assign) CGFloat letteringCharacterSpacing; -@property (nonatomic, assign) CGFloat letteringVerticalSpacing; -@property (nonatomic, assign) CGFloat letteringHorizontalSpacing; - -// Whether the number label is centered vertically or not (NO by default) -@property (nonatomic, assign) BOOL verticallyCenterNumberLabel; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m deleted file mode 100644 index d3df12ab45..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeButtonLabel.m +++ /dev/null @@ -1,186 +0,0 @@ -// -// TOPasscodeButtonLabel.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeButtonLabel.h" - -@interface TOPasscodeButtonLabel () - -@property (nonatomic, strong, readwrite) UILabel *numberLabel; -@property (nonatomic, strong, readwrite) UILabel *letteringLabel; - -@end - -@implementation TOPasscodeButtonLabel - -#pragma mark - View Setup - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _letteringVerticalSpacing = 6.0f; - _letteringCharacterSpacing = 3.0f; - _letteringHorizontalSpacing = 5.0f; - _numberLabelFont = [UIFont systemFontOfSize:37.5f weight:UIFontWeightThin]; - _letteringLabelFont = [UIFont systemFontOfSize:9.0f weight:UIFontWeightThin]; - [self setUpViews]; - } - - return self; -} - -- (void)setUpViews -{ - if (!self.numberLabel) { - self.numberLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.numberLabel.text = self.numberString; - self.numberLabel.textColor = self.textColor; - self.numberLabel.font = self.numberLabelFont; - [self.numberLabel sizeToFit]; - [self addSubview:self.numberLabel]; - } - - // Create the lettering string only if we have a lettering value for it - if (!self.letteringLabel && self.letteringString.length > 0) { - self.letteringLabel = [[UILabel alloc] initWithFrame:CGRectZero]; - self.letteringLabel.textColor = self.textColor; - self.letteringLabel.font = self.letteringLabelFont; - [self.letteringLabel sizeToFit]; - [self addSubview:self.letteringLabel]; - [self updateLetteringLabelText]; - } -} - -#pragma mark - View Layout - - -- (void)updateLetteringLabelText -{ - if (self.letteringString.length == 0) { - return; - } - - NSMutableAttributedString* attrStr = [[NSMutableAttributedString alloc] initWithString:self.letteringString]; - [attrStr addAttribute:NSKernAttributeName value:@(_letteringCharacterSpacing) range:NSMakeRange(0, attrStr.length-1)]; - self.letteringLabel.attributedText = attrStr; -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - CGSize viewSize = self.bounds.size; - - [self.numberLabel sizeToFit]; - [self.letteringLabel sizeToFit]; - - CGFloat numberVerticalHeight = self.numberLabelFont.capHeight; - CGFloat letteringVerticalHeight = self.letteringLabelFont.capHeight; - CGFloat textTotalHeight = (numberVerticalHeight+2.0f) + self.letteringVerticalSpacing + (letteringVerticalHeight+2.0f); - - CGRect frame = self.numberLabel.frame; - frame.size.height = ceil(numberVerticalHeight) + 2.0f; - frame.origin.x = ceilf((viewSize.width - frame.size.width) * 0.5f); - - if (!self.horizontalLayout && !self.verticallyCenterNumberLabel) { - frame.origin.y = floorf((viewSize.height - textTotalHeight) * 0.5f); - } - else { - frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); - } - self.numberLabel.frame = CGRectIntegral(frame); - - if (self.letteringLabel) { - CGFloat y = CGRectGetMaxY(frame); - y += self.letteringVerticalSpacing; - - frame = self.letteringLabel.frame; - frame.size.height = ceil(letteringVerticalHeight) + 2.0f; - - if (!self.horizontalLayout) { - frame.origin.y = floorf(y); - frame.origin.x = (viewSize.width - frame.size.width) * 0.5f; - } - else { - frame.origin.y = floorf((viewSize.height - frame.size.height) * 0.5f); - frame.origin.x = CGRectGetMaxX(self.numberLabel.frame) + self.letteringHorizontalSpacing; - } - - self.letteringLabel.frame = CGRectIntegral(frame); - } -} - -#pragma mark - Accessors - - -- (void)setTextColor:(UIColor *)textColor -{ - if (textColor == _textColor) { return; } - _textColor = textColor; - - self.numberLabel.textColor = _textColor; - self.letteringLabel.textColor = _textColor; -} -/***********************************************************/ - -- (void)setNumberString:(NSString *)numberString -{ - self.numberLabel.text = numberString; - [self setNeedsLayout]; -} - -- (NSString *)numberString { return self.numberLabel.text; } - -/***********************************************************/ - -- (void)setLetteringString:(NSString *)letteringString -{ - _letteringString = [letteringString copy]; - [self setUpViews]; - [self updateLetteringLabelText]; - [self setNeedsLayout]; -} - -/***********************************************************/ - -- (void)setLetteringCharacterSpacing:(CGFloat)letteringCharacterSpacing -{ - _letteringCharacterSpacing = letteringCharacterSpacing; - [self updateLetteringLabelText]; -} - -/***********************************************************/ - -- (void)setNumberLabelFont:(UIFont *)numberLabelFont -{ - if (_numberLabelFont == numberLabelFont) { return; } - _numberLabelFont = numberLabelFont; - self.numberLabel.font = _numberLabelFont; -} - -/***********************************************************/ - -- (void)setLetteringLabelFont:(UIFont *)letteringLabelFont -{ - if (_letteringLabelFont == letteringLabelFont) { return; } - _letteringLabelFont = letteringLabelFont; - self.letteringLabel.font = letteringLabelFont; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h deleted file mode 100644 index 5dc9ca2251..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.h +++ /dev/null @@ -1,46 +0,0 @@ -// -// TOPasscodeCircleView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -NS_ASSUME_NONNULL_BEGIN - -/** - A view containing two circle image views that can animate - between filled and hollow, whilst maintaining compatibility - with translucency views. - */ -@interface TOPasscodeCircleView : UIView - -/* The circle patterns used for neutral and highlighted states. */ -@property (nonatomic, strong) UIImage *circleImage; -@property (nonatomic, strong) UIImage *highlightedCircleImage; - -/* Whether the highlighted view is visible. */ -@property (nonatomic, assign) BOOL isHighlighted; - -/* Animate the circle to be highlighted */ -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m deleted file mode 100644 index 3bb5de974a..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeCircleView.m +++ /dev/null @@ -1,89 +0,0 @@ -// -// TOPasscodeCircleView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeCircleView.h" - -@interface TOPasscodeCircleView () -@property (nonatomic, strong) UIImageView *bottomView; -@property (nonatomic, strong) UIImageView *topView; -@end - -@implementation TOPasscodeCircleView - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - self.userInteractionEnabled = NO; - - self.bottomView = [[UIImageView alloc] initWithFrame:self.bounds]; - self.bottomView.userInteractionEnabled = NO; - self.bottomView.contentMode = UIViewContentModeCenter; - self.bottomView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - [self addSubview:self.bottomView]; - - self.topView = [[UIImageView alloc] initWithFrame:self.bounds]; - self.topView.userInteractionEnabled = NO; - self.topView.contentMode = UIViewContentModeCenter; - self.topView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - self.topView.alpha = 0.0f; - [self addSubview:self.topView]; - } - - return self; -} - -- (void)setIsHighlighted:(BOOL)isHighlighted -{ - [self setHighlighted:isHighlighted animated:NO]; -} - -- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated -{ - if (highlighted == self.isHighlighted) { return; } - - _isHighlighted = highlighted; - - void (^animationBlock)(void) = ^{ - self.topView.alpha = highlighted ? 1.0f : 0.0f; - }; - - if (!animated) { - animationBlock(); - return; - } - - [UIView animateWithDuration:0.45f animations:animationBlock]; -} - -- (void)setCircleImage:(UIImage *)circleImage -{ - _circleImage = circleImage; - self.bottomView.image = circleImage; -} - -- (void)setHighlightedCircleImage:(UIImage *)highlightedCircleImage -{ - _highlightedCircleImage = highlightedCircleImage; - self.topView.image = highlightedCircleImage; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h deleted file mode 100644 index a448d8ca0b..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// TOPasscodeFixedInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -@class TOPasscodeCircleView; - -/** - A basic content view showing a row of circles that can be used to represent - a fixed size passcode. - */ -@interface TOPasscodeFixedInputView : UIView - -/* The size of each circle in this view (Default is 16) */ -@property (nonatomic, assign) CGFloat circleDiameter; - -/* The spacing between each circle (Default is 25.0f) */ -@property (nonatomic, assign) CGFloat circleSpacing; - -/* The number of circles in this view (Default is 4) */ -@property (nonatomic, assign) NSInteger length; - -/* The number of highlighted circles */ -@property (nonatomic, assign) NSInteger highlightedLength; - -/* The circle views managed by this view */ -@property (nonatomic, strong, readonly) NSArray *circleViews; - -/* Init with a set number of circles */ -- (instancetype)initWithLength:(NSInteger)length; - -/* Set the number of highlighted circles */ -- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m deleted file mode 100644 index e4dac42805..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeFixedInputView.m +++ /dev/null @@ -1,171 +0,0 @@ -// -// TOPasscodeFixedInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeFixedInputView.h" -#import "TOPasscodeCircleView.h" -#import "TOPasscodeCircleImage.h" - -@interface TOPasscodeFixedInputView () - -@property (nonatomic, strong, readwrite) NSArray *circleViews; -@property (nonatomic, strong) UIImage *circleImage; -@property (nonatomic, strong) UIImage *highlightedCircleImage; - -@end - -@implementation TOPasscodeFixedInputView - -#pragma mark - Object Creation - - -- (instancetype)initWithLength:(NSInteger)length -{ - if (self = [self initWithFrame:CGRectZero]) { - _length = length; - } - - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _circleSpacing = 25.0f; - _circleDiameter = 16.0f; - _length = 4; - } - - return self; -} - -#pragma mark - View Configuration - - -- (void)sizeToFit -{ - // Resize the view to encompass the circles - CGRect frame = self.frame; - frame.size.width = (_circleDiameter * _length) + (_circleSpacing * (_length - 1)) + 2.0f; - frame.size.height = _circleDiameter + 2.0f; - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - CGRect frame = CGRectZero; - frame.size = (CGSize){self.circleDiameter + 2.0f, self.circleDiameter + 2.0f}; - - for (TOPasscodeCircleView *circleView in self.circleViews) { - circleView.frame = frame; - frame.origin.x += self.circleDiameter + self.circleSpacing; - } -} - -#pragma mark - State Configuration - - -- (void)setHighlightedLength:(NSInteger)highlightedLength animated:(BOOL)animated -{ - NSInteger i = 0; - for (TOPasscodeCircleView *circleView in self.circleViews) { - [circleView setHighlighted:(i < highlightedLength) animated:animated]; - i++; - } -} - -#pragma mark - Circle View Configuration - - -- (void)setCircleViewsForLength:(NSInteger)length -{ - NSMutableArray *circleViews = [NSMutableArray array]; - if (self.circleViews) { - [circleViews addObjectsFromArray:self.circleViews]; - } - - [UIView performWithoutAnimation:^{ - while (circleViews.count != length) { - // Remove any extra circle views - if (circleViews.count > length) { - TOPasscodeCircleView *lastCircle = circleViews.lastObject; - [lastCircle removeFromSuperview]; - [circleViews removeLastObject]; - continue; - } - - // Add any new circle views - TOPasscodeCircleView *newCircleView = [[TOPasscodeCircleView alloc] init]; - [self setImagesOfCircleView:newCircleView]; - [self addSubview:newCircleView]; - [circleViews addObject:newCircleView]; - } - - self.circleViews = [NSArray arrayWithArray:circleViews]; - [self setNeedsLayout]; - [self layoutIfNeeded]; - }]; -} - -- (void)setCircleImagesForDiameter:(CGFloat)diameter -{ - self.circleImage = [TOPasscodeCircleImage hollowCircleImageOfSize:diameter strokeWidth:1.2f padding:1.0f]; - self.highlightedCircleImage = [TOPasscodeCircleImage circleImageOfSize:diameter inset:0.5f padding:1.0f antialias:YES]; - - for (TOPasscodeCircleView *circleView in self.circleViews) { - [self setImagesOfCircleView:circleView]; - } -} - -- (void)setImagesOfCircleView:(TOPasscodeCircleView *)circleView -{ - circleView.circleImage = self.circleImage; - circleView.highlightedCircleImage = self.highlightedCircleImage; -} - -#pragma mark - Accessors - - -- (NSArray *)circleViews -{ - if (_circleViews) { return _circleViews; } - _circleViews = [NSArray array]; - [self setCircleViewsForLength:self.length]; - [self setCircleImagesForDiameter:self.circleDiameter]; - return _circleViews; -} - -- (void)setCircleDiameter:(CGFloat)circleDiameter -{ - if (circleDiameter == _circleDiameter) { return; } - _circleDiameter = circleDiameter; - [self setCircleImagesForDiameter:_circleDiameter]; - [self sizeToFit]; -} - -- (void)setLength:(NSInteger)length -{ - if (_length == length) { return; } - _length = length; - [self setCircleViewsForLength:length]; -} - -- (void)setHighlightedLength:(NSInteger)highlightedLength -{ - [self setHighlightedLength:highlightedLength animated:NO]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h deleted file mode 100644 index 05103b27d9..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.h +++ /dev/null @@ -1,111 +0,0 @@ -// -// TOPasscodeInputField.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -#import "TOPasscodeFixedInputView.h" -#import "TOPasscodeVariableInputView.h" - - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSInteger, TOPasscodeInputFieldStyle) { - TOPasscodeInputFieldStyleFixed, // The passcode explicitly requires a specific number of characters (Shows hollow circles) - TOPasscodeInputFieldStyleVariable // The passcode can be any arbitrary number of characters (Shows an empty rectangle) -}; - -/** - An abstract input view capable of receiving different types of passcodes. - When a fixed character passcode is specified, the view shows a row of circles. - When a variable passcode is specified, a rounded rectangle is shown. - */ -@interface TOPasscodeInputField : UIView - -/* The visual effects view used to control the vibrancy of the input field */ -@property (nonatomic, strong, readonly) UIVisualEffectView *visualEffectView; - -/* The input style of this control */ -@property (nonatomic, assign) TOPasscodeInputFieldStyle style; - -/* A row of hollow circles at a preset length. Valid only when `style` is set to `fixed` */ -@property (nonatomic, readonly, nullable) TOPasscodeFixedInputView *fixedInputView; - -/* A rounded rectangle representing a passcode of arbitrary length. Valid only when `style` is set to `variable`. */ -@property (nonatomic, readonly, nullable) TOPasscodeVariableInputView *variableInputView; - -/* The 'submit' button shown when `showSubmitButton` is true. */ -@property (nonatomic, readonly, nullable) UIButton *submitButton; - -/* Shows an 'OK' button next to the view when characters have been added. */ -@property (nonatomic, assign) BOOL showSubmitButton; - -/* The amount of spacing between the 'OK' button and the passcode field */ -@property (nonatomic, assign) CGFloat submitButtonSpacing; - -/* The amount of spacing between the 'OK' button and the passcode field */ -@property (nonatomic, assign) CGFloat submitButtonVerticalSpacing; - -/* The font size of the submit button */ -@property (nonatomic, assign) CGFloat submitButtonFontSize; - -/* The current passcode entered into this view */ -@property (nonatomic, copy, nullable) NSString *passcode; - -/* If this view is directly receiving input, this can change the `UIKeyboard` appearance. */ -@property (nonatomic, assign) UIKeyboardAppearance keyboardAppearance; - -/* The type of button used for the 'Done' button in the keyboard */ -@property(nonatomic, assign) UIReturnKeyType returnKeyType; - -/* The alpha value of the views in this view (For tranclucent styling) */ -@property (nonatomic, assign) CGFloat contentAlpha; - -/* Whether the view may be tapped to enable character input (Default is NO) */ -@property (nonatomic, assign) BOOL enabled; - -/** Called when the number of digits has been entered, or the user tapped 'Done' on the keyboard */ -@property (nonatomic, copy) void (^passcodeCompletedHandler)(NSString *code); - -/** Horizontal layout. The 'OK' button will be placed under the text field */ -@property (nonatomic, assign) BOOL horizontalLayout; - -/* Init with the target length needed for this passcode */ -- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style; - -/* Replace the passcode with this one, and animate the transition. */ -- (void)setPasscode:(nullable NSString *)passcode animated:(BOOL)animated; - -/* Add additional characters to the end of the passcode, and animate if desired. */ -- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated; - -/* Delete a number of characters from the end, animated if desired. */ -- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated; - -/* Plays a shaking animation and resets the passcode back to empty */ -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact; - -/* Animates the OK button changing location. */ -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration; - -@end - -NS_ASSUME_NONNULL_END diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m deleted file mode 100644 index 0b3227c0ea..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeInputField.m +++ /dev/null @@ -1,423 +0,0 @@ -// -// TOPasscodeInputField.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeInputField.h" - -#import "TOPasscodeVariableInputView.h" -#import "TOPasscodeFixedInputView.h" - -#import - -@interface TOPasscodeInputField () - -// Convenience getters -@property (nonatomic, readonly) UIView *inputField; // Returns whichever input field is currently visible -@property (nonatomic, readonly) NSInteger maximumPasscodeLength; // The mamximum number of characters allowed (0 if uncapped) - -@property (nonatomic, strong, readwrite) TOPasscodeFixedInputView *fixedInputView; -@property (nonatomic, strong, readwrite) TOPasscodeVariableInputView *variableInputView; -@property (nonatomic, strong, readwrite) UIButton *submitButton; -@property (nonatomic, strong, readwrite) UIVisualEffectView *visualEffectView; - -@end - -@implementation TOPasscodeInputField - -#pragma mark - View Set-up - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - [self setUp]; - [self setUpForStyle:TOPasscodeInputFieldStyleFixed]; - } - - return self; -} - -- (instancetype)initWithStyle:(TOPasscodeInputFieldStyle)style -{ - if (self = [self initWithFrame:CGRectZero]) { - _style = style; - [self setUp]; - [self setUpForStyle:style]; - } - - return self; -} - -- (void)setUp -{ - self.backgroundColor = [UIColor clearColor]; - _submitButtonSpacing = 4.0f; - _submitButtonVerticalSpacing = 5.0f; - - _visualEffectView = [[UIVisualEffectView alloc] initWithEffect:nil]; - [self addSubview:_visualEffectView]; -} - -- (void)setUpForStyle:(TOPasscodeInputFieldStyle)style -{ - if (self.inputField) { - [self.inputField removeFromSuperview]; - self.variableInputView = nil; - self.fixedInputView = nil; - } - - if (style == TOPasscodeInputFieldStyleVariable) { - self.variableInputView = [[TOPasscodeVariableInputView alloc] init]; - [self.visualEffectView.contentView addSubview:self.variableInputView]; - } - else { - self.fixedInputView = [[TOPasscodeFixedInputView alloc] init]; - [self.visualEffectView.contentView addSubview:self.fixedInputView]; - } - - // Set the frame for the currently visible input view - [self.inputField sizeToFit]; - - // Size this view to match - [self sizeToFit]; -} - -#pragma mark - View Layout - -- (void)sizeToFit -{ - // Resize the view to encompass the current input view - CGRect frame = self.frame; - [self.inputField sizeToFit]; - frame.size = self.inputField.frame.size; - if (self.horizontalLayout) { - frame.size.height += self.submitButtonVerticalSpacing + CGRectGetHeight(self.submitButton.frame); - } - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - self.visualEffectView.frame = self.inputField.bounds; - - if (!self.submitButton) { return; } - - [self.submitButton sizeToFit]; - [self bringSubviewToFront:self.submitButton]; - - CGRect frame = self.submitButton.frame; - if (!self.horizontalLayout) { - frame.origin.x = CGRectGetMaxX(self.bounds) + self.submitButtonSpacing; - frame.origin.y = (CGRectGetHeight(self.bounds) - CGRectGetHeight(frame)) * 0.5f; - } - else { - frame.origin.x = (CGRectGetWidth(self.frame) - frame.size.width) * 0.5f; - frame.origin.y = CGRectGetMaxY(self.inputField.frame) + self.submitButtonVerticalSpacing; - } - self.submitButton.frame = CGRectIntegral(frame); -} - -#pragma mark - Interaction - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesBegan:touches withEvent:event]; - if (!self.enabled) { return; } - self.contentAlpha = 0.5f; -} - -- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event -{ - [super touchesCancelled:touches withEvent:event]; - if (!self.enabled) { return; } - [UIView animateWithDuration:0.3f animations:^{ - self.contentAlpha = 1.0f; - }]; - [self becomeFirstResponder]; -} - -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event -{ - CGRect frame = self.bounds; - frame.size.width += self.submitButton.frame.size.width + (self.submitButtonSpacing * 2.0f); - frame.size.height += self.submitButtonVerticalSpacing; - - if (CGRectContainsPoint(frame, point)) { - return YES; - } - return NO; -} - -- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event { - - if ([[super hitTest:point withEvent:event] isEqual:self.submitButton]) { - if (CGRectContainsPoint(self.submitButton.frame, point)) { - return self.submitButton; - } else { - return self; - } - } - - return [super hitTest:point withEvent:event]; -} - -#pragma mark - Text Input Protocol - -- (BOOL)canBecomeFirstResponder { return self.enabled; } - -- (BOOL)hasText { return self.passcode.length > 0; } - -- (void)insertText:(NSString *)text -{ - if ([text isEqualToString:@"\n"]) { - if (self.passcodeCompletedHandler) { self.passcodeCompletedHandler(self.passcode); } - return; - } - - [self appendPasscodeCharacters:text animated:NO]; -} -- (void)deleteBackward -{ - [self deletePasscodeCharactersOfCount:1 animated:YES]; -} - -- (UIKeyboardType)keyboardType { return UIKeyboardTypeASCIICapable; } - -- (UITextAutocorrectionType)autocorrectionType { return UITextAutocorrectionTypeNo; } - -- (UIReturnKeyType)returnKeyType { return UIReturnKeyGo; } - -- (BOOL)enablesReturnKeyAutomatically { return YES; } - -#pragma mark - Text Input - -- (void)setPasscode:(NSString *)passcode animated:(BOOL)animated -{ - if (passcode == self.passcode) { return; } - _passcode = passcode; - - BOOL passcodeIsComplete = NO; - if (self.fixedInputView) { - [self.fixedInputView setHighlightedLength:_passcode.length animated:animated]; - passcodeIsComplete = _passcode.length >= self.maximumPasscodeLength; - } - else { - [self.variableInputView setLength:_passcode.length animated:animated]; - } - - if (self.submitButton) { - self.submitButton.hidden = (_passcode.length == 0); - [self bringSubviewToFront:self.submitButton]; - } - - if (passcodeIsComplete && self.passcodeCompletedHandler) { - self.passcodeCompletedHandler(_passcode); - } - - [self reloadInputViews]; -} - -- (void)appendPasscodeCharacters:(NSString *)characters animated:(BOOL)animated -{ - if (characters == nil) { return; } - if (self.maximumPasscodeLength > 0 && self.passcode.length >= self.maximumPasscodeLength) { return; } - - if (_passcode == nil) { _passcode = @""; } - [self setPasscode:[_passcode stringByAppendingString:characters] animated:animated]; -} - -- (void)deletePasscodeCharactersOfCount:(NSInteger)deleteCount animated:(BOOL)animated -{ - if (deleteCount <= 0 || self.passcode.length <= 0) { return; } - [self setPasscode:[self.passcode substringToIndex:(self.passcode.length - 1)] animated:animated]; -} - -- (void)resetPasscodeAnimated:(BOOL)animated playImpact:(BOOL)impact -{ - [self setPasscode:nil animated:animated]; - - // Play a negative impact effect - if (@available(iOS 9.0, *)) { - // https://stackoverflow.com/questions/41444274/how-to-check-if-haptic-engine-uifeedbackgenerator-is-supported - if (impact) { AudioServicesPlaySystemSoundWithCompletion(1521, nil); } - } - - if (!animated) { return; } - - CGPoint center = self.center; - CGPoint offset = center; - offset.x -= self.frame.size.width * 0.3f; - - // Play the view sliding out and then springing back in - id completionBlock = ^(BOOL finished) { - [UIView animateWithDuration:1.0f - delay:0.0f - usingSpringWithDamping:0.15f - initialSpringVelocity:10.0f - options:0 animations:^{ - self.center = center; - }completion:nil]; - }; - - [UIView animateWithDuration:0.05f animations:^{ - self.center = offset; - }completion:completionBlock]; - - if (!self.submitButton) { return; } - - [UIView animateWithDuration:0.7f animations:^{ - self.submitButton.alpha = 0.0f; - } completion:^(BOOL complete) { - self.submitButton.alpha = 1.0f; - self.submitButton.hidden = YES; - }]; -} - -#pragma mark - Button Callbacks - -- (void)submitButtonTapped:(id)sender -{ - if (self.passcodeCompletedHandler) { - self.passcodeCompletedHandler(self.passcode); - } -} - -#pragma mark - Private Accessors - -- (UIView *)inputField -{ - if (self.fixedInputView) { - return (UIView *)self.fixedInputView; - } - - return (UIView *)self.variableInputView; -} - -- (NSInteger)maximumPasscodeLength -{ - if (self.style == TOPasscodeInputFieldStyleFixed) { - return self.fixedInputView.length; - } - - return 0; -} - -#pragma mark - Public Accessors - - -- (void)setShowSubmitButton:(BOOL)showSubmitButton -{ - if (_showSubmitButton == showSubmitButton) { - return; - } - - _showSubmitButton = showSubmitButton; - - if (!_showSubmitButton) { - [self.submitButton removeFromSuperview]; - self.submitButton = nil; - return; - } - - self.submitButton = [UIButton buttonWithType:UIButtonTypeSystem]; - [self.submitButton setTitle:@"OK" forState:UIControlStateNormal]; - [self.submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; - [self.submitButton.titleLabel setFont:[UIFont systemFontOfSize:18.0f]]; - self.submitButton.hidden = YES; - [self addSubview:self.submitButton]; - - [self setNeedsLayout]; -} - -- (void)setSubmitButtonSpacing:(CGFloat)submitButtonSpacing -{ - if (submitButtonSpacing == _submitButtonSpacing) { return; } - _submitButtonSpacing = submitButtonSpacing; - [self setNeedsLayout]; -} - -- (void)setSubmitButtonFontSize:(CGFloat)submitButtonFontSize -{ - if (submitButtonFontSize == _submitButtonFontSize) { return; } - _submitButtonFontSize = submitButtonFontSize; - self.submitButton.titleLabel.font = [UIFont systemFontOfSize:_submitButtonFontSize]; - [self.submitButton sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setStyle:(TOPasscodeInputFieldStyle)style -{ - if (style == _style) { return; } - _style = style; - [self setUpForStyle:_style]; -} - -- (void)setPasscode:(NSString *)passcode -{ - [self setPasscode:passcode animated:NO]; -} - -- (void)setContentAlpha:(CGFloat)contentAlpha -{ - _contentAlpha = contentAlpha; - self.inputField.alpha = contentAlpha; - self.submitButton.alpha = contentAlpha; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout -{ - [self setHorizontalLayout:horizontalLayout animated:NO duration:0.0f]; -} - -- (void)setHorizontalLayout:(BOOL)horizontalLayout animated:(BOOL)animated duration:(CGFloat)duration -{ - if (_horizontalLayout == horizontalLayout) { - return; - } - - UIView *snapshotView = nil; - - if (self.submitButton && self.submitButton.hidden == NO && animated) { - snapshotView = [self.submitButton snapshotViewAfterScreenUpdates:NO]; - snapshotView.frame = self.submitButton.frame; - [self addSubview:snapshotView]; - } - - _horizontalLayout = horizontalLayout; - - if (!animated || !self.submitButton) { - [self sizeToFit]; - [self setNeedsLayout]; - return; - } - - self.submitButton.alpha = 0.0f; - [self setNeedsLayout]; - [self layoutIfNeeded]; - - id animationBlock = ^{ - self.submitButton.alpha = 1.0f; - snapshotView.alpha = 0.0f; - }; - - id completionBlock = ^(BOOL complete) { - [snapshotView removeFromSuperview]; - [self bringSubviewToFront:self.submitButton]; - }; - - [UIView animateWithDuration:duration animations:animationBlock completion:completionBlock]; -} - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h deleted file mode 100644 index 737f76761f..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// TOPasscodeVariableInputView.h -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 - -/** - A basic content view showing a rounded rectangle containing circles - that can be used to represent a variable size passcode. - */ -@interface TOPasscodeVariableInputView : UIImageView - -/* The thickness of the stroke around the view (Default is 1.5) */ -@property (nonatomic, assign) CGFloat outlineThickness; - -/* The corner radius of the stroke (Default is 5) */ -@property (nonatomic, assign) CGFloat outlineCornerRadius; - -/* The size of each circle bullet point representing a passcoded character (Default is 10) */ -@property (nonatomic, assign) CGFloat circleDiameter; - -/* The spacing between each circle (Default is 15) */ -@property (nonatomic, assign) CGFloat circleSpacing; - -/* The padding between the circles and the outer outline (Default is {10,10}) */ -@property (nonatomic, assign) CGSize outlinePadding; - -/* The maximum number of circles to show (This will indicate the view's width) (Default is 12) */ -@property (nonatomic, assign) NSInteger maximumVisibleLength; - -/* Set the number of characters entered into this view (May be larger than `maximumVisibleLength`) */ -@property (nonatomic, assign) NSInteger length; - -/* Set the number of characters represented by this field, animated if desired */ -- (void)setLength:(NSInteger)length animated:(BOOL)animated; - -@end diff --git a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m b/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m deleted file mode 100644 index 1dc0783b91..0000000000 --- a/iOSClient/Utility/TOPasscodeViewController/Views/Shared/TOPasscodeVariableInputView.m +++ /dev/null @@ -1,254 +0,0 @@ -// -// TOPasscodeVariableInputView.m -// -// Copyright 2017 Timothy Oliver. All rights reserved. -// -// 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 "TOPasscodeVariableInputView.h" -#import "TOPasscodeCircleImage.h" - -@interface TOPasscodeVariableInputView () - -@property (nonatomic, strong) UIImage *backgroundImage; // The outline image for this view -@property (nonatomic, strong) UIImage *circleImage; // The circle image representing a single character - -@property (nonatomic, strong) NSMutableArray *circleViews; - -@end - -@implementation TOPasscodeVariableInputView - -#pragma mark - Class Creation - - -- (instancetype)initWithFrame:(CGRect)frame -{ - if (self = [super initWithFrame:frame]) { - _outlineThickness = 1.0f; - _outlineCornerRadius = 5.0f; - _circleDiameter = 11.0f; - _circleSpacing = 7.0f; - _outlinePadding = (CGSize){10,10}; - _maximumVisibleLength = 12; - } - - return self; -} - -#pragma mark - View Setup - -- (void)setUpImageForCircleViews -{ - if (self.circleImage != nil) { return; } - - self.circleImage = [TOPasscodeCircleImage circleImageOfSize:_circleDiameter inset:0.0f padding:1.0f antialias:YES]; - for (UIImageView *circleView in self.circleViews) { - circleView.image = self.circleImage; - [circleView sizeToFit]; - } -} - -- (void)setUpCircleViewsForLength:(NSInteger)length -{ - // Set up the number of circle views if needed - if (self.circleViews.count == length) { return; } - - if (self.circleViews == nil) { - self.circleViews = [NSMutableArray arrayWithCapacity:_maximumVisibleLength]; - } - - // Reduce the number of views - while (self.circleViews.count > length) { - UIImageView *circleView = self.circleViews.lastObject; - [circleView removeFromSuperview]; - [self.circleViews removeLastObject]; - } - - // Increase the number of views - [UIView performWithoutAnimation:^{ - while (self.circleViews.count < length) { - UIImageView *circleView = [[UIImageView alloc] initWithImage:self.circleImage]; - circleView.alpha = 0.0f; - [self addSubview:circleView]; - [self.circleViews addObject:circleView]; - } - }]; -} - -- (void)setUpBackgroundImage -{ - if (self.backgroundImage != nil) { return; } - - self.backgroundImage = [[self class] backgroundImageWithThickness:_outlineThickness cornerRadius:_outlineCornerRadius]; - self.image = self.backgroundImage; -} - -#pragma mark - View Layout - - -- (void)sizeToFit -{ - CGRect frame = self.frame; - - // Calculate the width - frame.size.width = self.outlineThickness * 2.0f; - frame.size.width += (self.outlinePadding.width * 2.0f); - frame.size.width += (self.maximumVisibleLength * (self.circleDiameter+2.0f)); // +2 for padding - frame.size.width += ((self.maximumVisibleLength - 1) * self.circleSpacing); - - // Height - frame.size.height = self.outlineThickness * 2.0f; - frame.size.height += self.outlinePadding.height * 2.0f; - frame.size.height += self.circleDiameter; - - self.frame = CGRectIntegral(frame); -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - - // Genearate the background image if we don't have one yet - [self setUpBackgroundImage]; - - // Set up the circle view image - [self setUpImageForCircleViews]; - - // Set up the circle views - [self setUpCircleViewsForLength:self.maximumVisibleLength]; - - // Layout the circle views for the current length - CGRect frame = CGRectZero; - frame.size = self.circleImage.size; - frame.origin.y = CGRectGetMidY(self.bounds) - (frame.size.height * 0.5f); - frame.origin.x = self.outlinePadding.width + self.outlineThickness; - - for (UIImageView *circleView in self.circleViews) { - circleView.frame = frame; - frame.origin.x += frame.size.width + self.circleSpacing; - } -} - -#pragma mark - Accessors - - -- (void)setOutlineThickness:(CGFloat)outlineThickness -{ - if (_outlineThickness == outlineThickness) { return; } - _outlineThickness = outlineThickness; - self.backgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setOutlineCornerRadius:(CGFloat)outlineCornerRadius -{ - if (_outlineCornerRadius == outlineCornerRadius) { return; } - _outlineCornerRadius = outlineCornerRadius; - self.backgroundImage = nil; - [self setNeedsLayout]; -} - -- (void)setCircleDiameter:(CGFloat)circleDiameter -{ - if (_circleDiameter == circleDiameter) { return; } - _circleDiameter = circleDiameter; - self.circleImage = nil; - [self setUpImageForCircleViews]; -} - -- (void)setCircleSpacing:(CGFloat)circleSpacing -{ - if (_circleSpacing == circleSpacing) { return; } - _circleSpacing = circleSpacing; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setOutlinePadding:(CGSize)outlinePadding -{ - if (CGSizeEqualToSize(outlinePadding, _outlinePadding)) { return; } - _outlinePadding = outlinePadding; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setMaximumVisibleLength:(NSInteger)maximumVisibleLength -{ - if (_maximumVisibleLength == maximumVisibleLength) { return; } - _maximumVisibleLength = maximumVisibleLength; - [self setUpCircleViewsForLength:maximumVisibleLength]; - [self sizeToFit]; - [self setNeedsLayout]; -} - -- (void)setLength:(NSInteger)length -{ - [self setLength:length animated:NO]; -} - -- (void)setLength:(NSInteger)length animated:(BOOL)animated -{ - if (length == _length) { return; } - - _length = length; - - void (^animationBlock)(void) = ^{ - NSInteger i = 0; - for (UIImageView *circleView in self.circleViews) { - circleView.alpha = i < length ? 1.0f : 0.0f; - i++; - } - }; - - if (!animated) { - animationBlock(); - return; - } - - [UIView animateWithDuration:0.4f animations:animationBlock]; -} - -#pragma mark - Image Creation - - -+ (UIImage *)backgroundImageWithThickness:(CGFloat)thickness cornerRadius:(CGFloat)radius -{ - CGFloat inset = thickness / 2.0f; - CGFloat dimension = (radius * 2.0f) + 2.0f; - - CGRect frame = CGRectZero; - frame.origin = CGPointMake(inset, inset); - frame.size = CGSizeMake(dimension, dimension); - - CGSize canvasSize = frame.size; - canvasSize.width += thickness; - canvasSize.height += thickness; - - UIGraphicsBeginImageContextWithOptions(canvasSize, NO, 0.0f); - { - UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius]; - path.lineWidth = thickness; - [[UIColor blackColor] setStroke]; - [path stroke]; - } - - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); - - UIEdgeInsets insets = UIEdgeInsetsMake(radius+1, radius+1, radius+1, radius+1); - image = [image resizableImageWithCapInsets:insets]; - return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; -} - -@end