├── NOTICE
├── Sources
└── Authenticator
│ ├── Resources
│ └── media.xcassets
│ │ ├── Contents.json
│ │ └── passkey.imageset
│ │ └── Contents.json
│ ├── Models
│ ├── Internal
│ │ ├── AuthenticatorMFAType.swift
│ │ ├── InputType.swift
│ │ ├── Credentials.swift
│ │ ├── Validator.swift
│ │ └── AuthenticatorStateProtocol.swift
│ ├── AuthenticationFlow.swift
│ ├── PasskeyPrompt.swift
│ ├── AuthenticatorMessage.swift
│ └── AuthFactor.swift
│ ├── Constants
│ └── ComponentInformation.swift
│ ├── Service
│ ├── AmplifyAuthenticationService.swift
│ └── AuthenticationService.swift
│ ├── Extensions
│ ├── Logger+Error.swift
│ ├── AuthUserAttributeKey+LocalizedTitle.swift
│ ├── DeliveryDestination+Value.swift
│ ├── Bundle+Utils.swift
│ ├── AuthError+Connectivity.swift
│ ├── String+Localizable.swift
│ ├── Color+Utils.swift
│ └── EnvironmentValues+Authenticator.swift
│ ├── Utilities
│ ├── Platform.swift
│ ├── AuthenticatorLogging.swift
│ ├── Padding.swift
│ ├── KeyboardIterableFields.swift
│ └── AuthenticatorField.swift
│ ├── Configuration
│ └── AuthenticatorOptions.swift
│ ├── Options
│ └── TOTPOptions.swift
│ ├── Views
│ ├── ErrorView.swift
│ ├── ContinueSignInWithTOTPCopyKeyView.swift
│ ├── Internal
│ │ ├── AuthenticatorTextWithHeader.swift
│ │ ├── AuthenticatorView.swift
│ │ ├── DefaultHeader.swift
│ │ ├── ConfirmSignInWithCodeView.swift
│ │ ├── SignUpInputField.swift
│ │ └── AuthenticatorMessageView.swift
│ ├── Primitives
│ │ ├── RadioButton.swift
│ │ ├── ImageButton.swift
│ │ └── TextField.swift
│ ├── ContinueSignInWithTOTPSetupQRCodeView.swift
│ ├── ConfirmSignInWithCustomChallengeView.swift
│ ├── ConfirmSignInWithTOTPCodeView.swift
│ ├── ConfirmSignInWithOTPView.swift
│ ├── ConfirmSignInWithMFACodeView.swift
│ ├── VerifyUserView.swift
│ └── PromptToCreatePasskeyView.swift
│ └── States
│ ├── SignedInState.swift
│ ├── ContinueSignInWithEmailMFASetupState.swift
│ ├── ConfirmSignInWithNewPasswordState.swift
│ ├── ContinueSignInWithMFASelectionState.swift
│ ├── ResetPasswordState.swift
│ ├── ContinueSignInWithMFASetupSelectionState.swift
│ ├── SignInConfirmPasswordState.swift
│ ├── ConfirmSignInWithCodeState.swift
│ ├── PasskeyCreatedState.swift
│ ├── ConfirmResetPasswordState.swift
│ ├── VerifyUserState.swift
│ ├── ConfirmSignUpState.swift
│ ├── PromptToCreatePasskeyState.swift
│ ├── ContinueSignInWithTOTPSetupState.swift
│ ├── SignInState.swift
│ └── ConfirmVerifyUserState.swift
├── Tests
├── AuthenticatorHostApp
│ ├── AuthenticatorHostApp
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ ├── AuthenticatorHostApp.entitlements
│ │ └── Utils
│ │ │ └── AuthCategoryConfigurationFactory.swift
│ ├── AuthenticatorHostApp.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── AuthenticatorHostApp.xcscheme
│ ├── AuthenticatorHostAppUITests
│ │ ├── TestCases
│ │ │ ├── __Snapshots__
│ │ │ │ ├── SignUpViewTests
│ │ │ │ │ ├── testDefaultSignUpView.1.png
│ │ │ │ │ └── testPasswordlessSignUpView.1.png
│ │ │ │ ├── PasskeyPromptTests
│ │ │ │ │ ├── testSignInPasskeyPrompt.1.png
│ │ │ │ │ ├── testSignUpPasskeyPrompt.1.png
│ │ │ │ │ └── testSignInPasskeyCreated.1.png
│ │ │ │ ├── SignInViewTests
│ │ │ │ │ ├── testSignInViewWithoutSignUp.1.png
│ │ │ │ │ └── testSignInViewWithWithUsernameAsPhoneNumber.1.png
│ │ │ │ ├── Untitled
│ │ │ │ │ └── testConfirmSignInWithCustomAuthChallenge.1.png
│ │ │ │ ├── ResetPasswordViewTests
│ │ │ │ │ ├── testConfirmSignInWithResetPassword.1.png
│ │ │ │ │ └── testResetPasswordViewWithUsernameAsPhoneNumber.1.png
│ │ │ │ ├── ConfirmSignInWithOTPCodeViewTests
│ │ │ │ │ ├── testConfirmSignInWithEmail.1.png
│ │ │ │ │ └── testConfirmSignInWithSMS.1.png
│ │ │ │ ├── ConfirmSignInWithNewPasswordTests
│ │ │ │ │ └── testConfirmSignInWithNewPassword.1.png
│ │ │ │ ├── ConfirmSignInWithConfirmSignUpTests
│ │ │ │ │ └── testConfirmSignInWithConfirmSignUp.1.png
│ │ │ │ ├── ConfirmSignInWithTOTPCodeViewTests
│ │ │ │ │ └── testConfirmSignInWithTOTPCodeView.1.png
│ │ │ │ ├── ContinueSignInWithTOTPSetupViewTests
│ │ │ │ │ └── testContinueSignInWithTOTPSetupView.1.png
│ │ │ │ ├── ContinueSignInWithEmailMFASetupViewTests
│ │ │ │ │ └── testContinueSignInWithEmailMFASetupView.1.png
│ │ │ │ ├── ContinueSignInWithMFASelectionViewTests
│ │ │ │ │ └── testContinueSignInWithMFASelectionView.1.png
│ │ │ │ ├── ConfirmSignInWithCustomAuthChallengeViewTests
│ │ │ │ │ └── testConfirmSignInWithCustomAuthChallenge.1.png
│ │ │ │ ├── ContinueSignInWithFirstFactorSelectionTests
│ │ │ │ │ └── testContinueSignInWithFirstFactorSelection.1.png
│ │ │ │ └── ContinueSignInWithMFASetupSelectionViewTests
│ │ │ │ │ └── testContinueSignInWithMFASetupSelectionView.1.png
│ │ │ ├── ConfirmSignInWithConfirmSignUpTests.swift
│ │ │ ├── ConfirmSignInWithTOTPCodeViewTests.swift
│ │ │ ├── ConfirmSignInWithNewPasswordTests.swift
│ │ │ ├── ContinueSignInWithTOTPSetupViewTests.swift
│ │ │ ├── ContinueSignInWithMFASelectionViewTests.swift
│ │ │ ├── ContinueSignInWithEmailMFASetupViewTests.swift
│ │ │ ├── ConfirmSignInWithCustomAuthChallengeViewTests.swift
│ │ │ ├── ContinueSignInWithFirstFactorSelectionTests.swift
│ │ │ ├── ContinueSignInWithMFASetupSelectionViewTests.swift
│ │ │ ├── SignUpViewTests.swift
│ │ │ ├── SignInViewTests.swift
│ │ │ ├── ResetPasswordViewTests.swift
│ │ │ ├── ConfirmSignInWithOTPCodeViewTests.swift
│ │ │ └── PasskeyPromptTests.swift
│ │ ├── SnapshotLogic
│ │ │ └── CleanCounterBetweenTestCases.swift
│ │ ├── AuthenticatorUITestUtils.swift
│ │ └── AuthenticatorBaseTestCase.swift
│ └── AuthenticatorHostApp.xctestplan
└── AuthenticatorTests
│ ├── States
│ ├── SignedInStateTests.swift
│ ├── ResetPasswordStateTests.swift
│ ├── ConfirmSignInWithNewPasswordStateTests.swift
│ ├── ContinueSignInWithEmailMFASetupStateTests.swift
│ ├── ContinueSignInWithMFASelectionStateTests.swift
│ ├── ConfirmResetPasswordStateTests.swift
│ └── ContinueSignInWithMFASetupSelectionStateTests.swift
│ └── Mocks
│ └── MockAuthenticatorState.swift
├── .github
├── CODEOWNERS
└── workflows
│ ├── notify_release.yml
│ ├── dependency-review.yml
│ ├── ui_tests.yml
│ ├── issue_closed.yml
│ ├── unit_tests.yml
│ ├── issue_comment.yml
│ └── issue_opened.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── Authenticator.xcscheme
├── codecov.yml
├── CODE_OF_CONDUCT.md
├── Package.swift
├── .gitignore
├── README.md
├── CONTRIBUTING.md
└── CHANGELOG.md
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Resources/media.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
2 | * @aws-amplify/amplify-ios
3 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Resources/media.xcassets/passkey.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "passkey.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testDefaultSignUpView.1.png
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | - "Sources/Authenticator/Views"
3 | - "Sources/Authenticator/Theming"
4 | - "Sources/Authenticator/Authenticator.swift"
5 |
6 | codecov:
7 | branch: main
8 |
9 | coverage:
10 | status:
11 | patch: off
12 | project:
13 | default:
14 | threshold: 1%
15 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignInPasskeyPrompt.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignInPasskeyPrompt.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignUpPasskeyPrompt.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignUpPasskeyPrompt.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testPasswordlessSignUpView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignUpViewTests/testPasswordlessSignUpView.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignInPasskeyCreated.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/PasskeyPromptTests/testSignInPasskeyCreated.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithoutSignUp.1.png
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/Internal/AuthenticatorMFAType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | enum AuthenticatorFactorType {
9 | case sms
10 | case email
11 | case totp
12 | case none
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/Untitled/testConfirmSignInWithCustomAuthChallenge.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/Untitled/testConfirmSignInWithCustomAuthChallenge.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/AuthenticatorHostApp.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/Internal/InputType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | enum InputType {
11 | case text
12 | case password
13 | case date
14 | case phoneNumber
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testConfirmSignInWithResetPassword.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testConfirmSignInWithResetPassword.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithEmail.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithEmail.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithSMS.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithOTPCodeViewTests/testConfirmSignInWithSMS.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/SignInViewTests/testSignInViewWithWithUsernameAsPhoneNumber.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithNewPasswordTests/testConfirmSignInWithNewPassword.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithNewPasswordTests/testConfirmSignInWithNewPassword.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithConfirmSignUpTests/testConfirmSignInWithConfirmSignUp.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithConfirmSignUpTests/testConfirmSignInWithConfirmSignUp.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithTOTPCodeViewTests/testConfirmSignInWithTOTPCodeView.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ResetPasswordViewTests/testResetPasswordViewWithUsernameAsPhoneNumber.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithTOTPSetupViewTests/testContinueSignInWithTOTPSetupView.1.png
--------------------------------------------------------------------------------
/Sources/Authenticator/Constants/ComponentInformation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | public class ComponentInformation {
11 | public static let version = "1.3.0"
12 | public static let name = "amplify-ui-swift-authenticator"
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithEmailMFASetupViewTests/testContinueSignInWithEmailMFASetupView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithEmailMFASetupViewTests/testContinueSignInWithEmailMFASetupView.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASelectionViewTests/testContinueSignInWithMFASelectionView.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithCustomAuthChallengeViewTests/testConfirmSignInWithCustomAuthChallenge.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ConfirmSignInWithCustomAuthChallengeViewTests/testConfirmSignInWithCustomAuthChallenge.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithFirstFactorSelectionTests/testContinueSignInWithFirstFactorSelection.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithFirstFactorSelectionTests/testContinueSignInWithFirstFactorSelection.1.png
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASetupSelectionViewTests/testContinueSignInWithMFASetupSelectionView.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-amplify/amplify-ui-swift-authenticator/HEAD/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/__Snapshots__/ContinueSignInWithMFASetupSelectionViewTests/testContinueSignInWithMFASetupSelectionView.1.png
--------------------------------------------------------------------------------
/Sources/Authenticator/Service/AmplifyAuthenticationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | typealias AmplifyAuthenticationService = AuthCategory
12 |
13 | extension AuthenticationService where Self == AmplifyAuthenticationService {
14 | static var `default`: AuthenticationService {
15 | return Amplify.Auth
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/Logger+Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import Foundation
10 |
11 | extension Logger {
12 | func error(_ error: Error) {
13 | self.error(error: error)
14 | self.error(String(reflecting: error))
15 | }
16 |
17 | func verbose(_ error: Error) {
18 | self.verbose(String(reflecting: error))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/AuthUserAttributeKey+LocalizedTitle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 |
10 | extension AuthUserAttributeKey {
11 | var localizedTitle: String {
12 | switch self {
13 | case .email:
14 | return "authenticator.field.email.label".localized()
15 | case .phoneNumber:
16 | return "authenticator.field.phoneNumber.label".localized()
17 | default:
18 | return ""
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithConfirmSignUpTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ConfirmSignInWithConfirmSignUpTests: AuthenticatorBaseTestCase {
11 |
12 | func testConfirmSignInWithConfirmSignUp() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.confirmSignUp)
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithTOTPCodeViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ConfirmSignInWithTOTPCodeViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testConfirmSignInWithTOTPCodeView() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.confirmSignInWithTOTPCode)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Service/AuthenticationService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import class Amplify.AuthCategory
9 | import protocol Amplify.AmplifyError
10 | import protocol Amplify.AuthCategoryBehavior
11 | import enum Amplify.AuthError
12 | import enum Amplify.AuthUserAttributeKey
13 | import enum Amplify.AuthSignInStep
14 | import Foundation
15 | import SwiftUI
16 |
17 | protocol AuthenticationService: AuthCategoryBehavior, AnyObject { }
18 |
19 | extension Amplify.AuthCategory: AuthenticationService, @retroactive ObservableObject {}
20 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithNewPasswordTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ConfirmSignInWithNewPasswordTests: AuthenticatorBaseTestCase {
11 |
12 | func testConfirmSignInWithNewPassword() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.confirmSignInWithNewPassword)
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/DeliveryDestination+Value.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 |
10 | extension DeliveryDestination {
11 | var value: String? {
12 | switch self {
13 | case .email(let destination):
14 | return destination
15 | case .phone(let destination):
16 | return destination
17 | case .sms(let destination):
18 | return destination
19 | case .unknown(let destination):
20 | return destination
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithTOTPSetupViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ContinueSignInWithTOTPSetupViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testContinueSignInWithTOTPSetupView() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.continueSignInWithTOTPSetup)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASelectionViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ContinueSignInWithMFASelectionViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testContinueSignInWithMFASelectionView() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.continueSignInWithMFASelection)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithEmailMFASetupViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ContinueSignInWithEmailMFASetupViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testContinueSignInWithEmailMFASetupView() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.continueSignInWithEmailMFASetup)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/AuthenticationFlow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | /// Represents the authentication flow configuration for the Authenticator
11 | public enum AuthenticationFlow: Equatable {
12 | /// Password-only authentication flow
13 | case password
14 |
15 | /// User choice authentication flow with optional preferred factor and passkey prompts
16 | case userChoice(preferredAuthFactor: AuthFactor? = nil, passkeyPrompts: PasskeyPrompts = .init())
17 | }
18 |
19 | extension AuthenticationFlow: Codable {}
20 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Utilities/Platform.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | enum Platform {
11 | case macOS
12 | case iOS
13 | case unsupported
14 |
15 | static var current: Platform {
16 | #if os(macOS)
17 | return .macOS
18 | #elseif os(iOS)
19 | return .iOS
20 | #else
21 | return .unsupported
22 | #endif
23 | }
24 |
25 | static var isMacOS: Bool {
26 | current == .macOS
27 | }
28 |
29 | static var isIOS: Bool {
30 | current == .iOS
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithCustomAuthChallengeViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ConfirmSignInWithCustomAuthChallengeViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testConfirmSignInWithCustomAuthChallenge() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.confirmSignInWithCustomChallenge)
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithFirstFactorSelectionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ContinueSignInWithFirstFactorSelectionTests: AuthenticatorBaseTestCase {
11 |
12 | func testContinueSignInWithFirstFactorSelection() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.continueSignInWithFirstFactorSelection)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ContinueSignInWithMFASetupSelectionViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ContinueSignInWithMFASetupSelectionViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testContinueSignInWithMFASetupSelectionView() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.continueSignInWithMFASetupSelection)
17 | ])
18 | assertSnapshot()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/notify_release.yml:
--------------------------------------------------------------------------------
1 | name: Notify Release
2 |
3 | on:
4 | release:
5 | types: [released]
6 |
7 | permissions: {}
8 |
9 | jobs:
10 | notify:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Run webhook curl command
14 | env:
15 | WEBHOOK_URL: ${{ secrets.SLACK_RELEASE_WEBHOOK_URL }}
16 | VERSION: ${{github.event.release.html_url}}
17 | REPO_NAME: ${{github.event.repository.name}}
18 | ACTION_NAME: ${{github.event.action}}
19 | shell: bash
20 | run: echo $VERSION | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"action":"'$ACTION_NAME'", "repo":"'$REPO_NAME'", "version":"{}"}'
21 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Configuration/AuthenticatorOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | class AuthenticatorOptions: ObservableObject {
11 | @Published var hidesSignUpButton = false
12 | @Published var contentAnimation: Animation = .easeInOut(duration: 0.25)
13 | @Published var contentTransition: AnyTransition = .opacity
14 | @Published var signUpFields: [SignUpField] = []
15 | @Published var busyStyle = BusyStyle(content: ProgressView())
16 |
17 | struct BusyStyle {
18 | var blurRadius: CGFloat = 2
19 | var content: any View
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignUpViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class SignUpViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testDefaultSignUpView() throws {
13 | launchApp(with: [
14 | .initialStep(.signUp),
15 | ])
16 | assertSnapshot()
17 | }
18 |
19 | func testPasswordlessSignUpView() throws {
20 | launchApp(with: [
21 | .initialStep(.signUp),
22 | .passwordlessFlow(true)
23 | ])
24 | assertSnapshot()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | name: Dependency Review
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | dependency-review:
13 | name: Dependency Review
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout Code
17 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
18 | with:
19 | persist-credentials: false
20 |
21 | - name: Dependency Review
22 | uses: actions/dependency-review-action@7d90b4f05fea31dde1c4a1fb3fa787e197ea93ab # v3.0.7
23 | with:
24 | config-file: aws-amplify/amplify-ci-support/.github/dependency-review-config.yml@main
25 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/Internal/Credentials.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | class Credentials: ObservableObject {
12 | @Published var username: String = ""
13 | @Published var password: String?
14 |
15 | @Published var message: AuthenticatorMessage?
16 |
17 | /// Tracks the currently selected auth factor during sign-in.
18 | /// Used to detect when user changes their auth factor selection after already selecting one.
19 | /// When non-nil, subsequent factor selections require restarting the sign-in flow.
20 | @Published var selectedAuthFactor: AuthFactor?
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Utilities/AuthenticatorLogging.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 |
10 | protocol AuthenticatorLogging {
11 | static var log: Logger { get }
12 | var log: Logger { get }
13 | }
14 |
15 | extension AuthenticatorLogging {
16 | static var log: Logger {
17 | var category = String(describing: self)
18 | if let index = category.firstIndex(of: "<") {
19 | category = String(category.prefix(upTo: index))
20 | }
21 |
22 | return Amplify.Logging.logger(forCategory: category)
23 | }
24 |
25 | var log: Logger {
26 | type(of: self).log
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/Bundle+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | extension Bundle {
11 |
12 | var applicationName: String? {
13 | if let localizedName = Bundle.main.infoDictionary?[kCFBundleLocalizationsKey as String] as? String {
14 | return localizedName
15 | }
16 | if let displayName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String {
17 | return displayName
18 | }
19 | if let bundleName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String {
20 | return bundleName
21 | }
22 | return nil
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/AuthError+Connectivity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import Foundation
10 |
11 | extension AuthError {
12 | var isConnectivityError: Bool {
13 | guard let error = underlyingError as? NSError else {
14 | return false
15 | }
16 |
17 | let networkErrorCodes = [
18 | NSURLErrorCannotFindHost,
19 | NSURLErrorCannotConnectToHost,
20 | NSURLErrorNetworkConnectionLost,
21 | NSURLErrorDNSLookupFailed,
22 | NSURLErrorNotConnectedToInternet
23 | ]
24 | return networkErrorCodes.contains(where: { $0 == error.code })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Options/TOTPOptions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 | import Foundation
8 |
9 | /// Options for configuring the TOTP MFA Experience
10 | public struct TOTPOptions {
11 |
12 | /// The `issuer` is the title displayed in a user's TOTP App preceding the
13 | /// account name. In most cases, this should be the name of your app.
14 | /// For example, if your app is called "My App", your user will see
15 | /// "My App" - "username" in their TOTP app.
16 | public let issuer: String?
17 |
18 | /// Creates a `TOTPOptions`
19 | /// - Parameter issuer: The `issuer` is the title displayed in a user's TOTP App
20 | public init(issuer: String? = nil) {
21 | self.issuer = issuer
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/SignInViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class SignInViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testSignInViewWithWithUsernameAsPhoneNumber() throws {
13 | launchApp(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .userAttributes([ .phoneNumber ])
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | func testSignInViewWithoutSignUp() throws {
22 | launchApp(with: [
23 | .hidesSignUpButton(true),
24 | .initialStep(.signIn),
25 | .userAttributes([ .phoneNumber ])
26 | ])
27 | assertSnapshot()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/String+Localizable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 | /// Looks for a localized value using this value as the key.
12 | /// If no localization is found in the current app's bundle,
13 | /// it defaults to the one provided by Authenticator
14 | func localized(comment: String = "") -> String {
15 | let defaultValue = NSLocalizedString(self, bundle: .module, comment: "")
16 | return NSLocalizedString(
17 | self,
18 | bundle: .main,
19 | value: defaultValue,
20 | comment: ""
21 | )
22 | }
23 |
24 | func localized(using arguments: CVarArg...) -> String {
25 | return String(format: localized(), arguments)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ResetPasswordViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ResetPasswordViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testResetPasswordViewWithUsernameAsPhoneNumber() throws {
13 | launchApp(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.resetPassword),
16 | .userAttributes([ .phoneNumber ])
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | func testConfirmSignInWithResetPassword() throws {
22 | launchAppAndLogin(with: [
23 | .hidesSignUpButton(false),
24 | .initialStep(.signIn),
25 | .authSignInStep(.resetPassword)
26 | ])
27 | assertSnapshot()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "6A2C8881-BF18-41B8-8578-D487AB88126F",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "targetForVariableExpansion" : {
13 | "containerPath" : "container:AuthenticatorHostApp.xcodeproj",
14 | "identifier" : "483E093C2ABBC0D800EFD1D7",
15 | "name" : "AuthenticatorHostApp"
16 | },
17 | "testExecutionOrdering" : "random"
18 | },
19 | "testTargets" : [
20 | {
21 | "skippedTests" : [
22 | "AuthenticatorBaseTestCase"
23 | ],
24 | "target" : {
25 | "containerPath" : "container:AuthenticatorHostApp.xcodeproj",
26 | "identifier" : "483E095B2ABBC4AC00EFD1D7",
27 | "name" : "AuthenticatorHostAppUITests"
28 | }
29 | }
30 | ],
31 | "version" : 1
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/ConfirmSignInWithOTPCodeViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class ConfirmSignInWithOTPCodeViewTests: AuthenticatorBaseTestCase {
11 |
12 | func testConfirmSignInWithEmail() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.confirmSignInWithEmailMFACode)
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | func testConfirmSignInWithSMS() throws {
22 | launchAppAndLogin(with: [
23 | .hidesSignUpButton(false),
24 | .initialStep(.signIn),
25 | .authSignInStep(.confirmSignInWithSMSMFACode)
26 | ])
27 | assertSnapshot()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/error`` step.
11 | public struct ErrorView: View {
12 | @Environment(\.authenticatorTheme) private var theme
13 |
14 | public init() {}
15 |
16 | public var body: some View {
17 | AuthenticatorView(isBusy: false) {
18 | DefaultHeader(
19 | title: "authenticator.authenticatorError.title".localized()
20 | )
21 | .foregroundColor(theme.colors.border.error)
22 |
23 | SwiftUI.Text("authenticator.authenticatorError.message".localized())
24 | .foregroundColor(theme.colors.foreground.error)
25 |
26 | Spacer()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/ui_tests.yml:
--------------------------------------------------------------------------------
1 | name: Run UI Tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | ui-test-ios:
14 | runs-on: macos-15
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
18 |
19 | - name: Resolve and update Swift packages
20 | run: xcodebuild -resolvePackageDependencies -scheme Authenticator
21 |
22 | - name: UI test Authenticator on iOS
23 | working-directory: Tests/AuthenticatorHostApp
24 | run: |
25 | xcodebuild -resolvePackageDependencies -scheme Authenticator
26 | xcodebuild test -scheme AuthenticatorHostApp -sdk 'iphonesimulator' -destination 'platform=iOS Simulator,name=iPhone 16 Pro Max,OS=latest' -derivedDataPath Build/ -clonedSourcePackagesDirPath ~/Library/Developer/Xcode/DerivedData/Authenticator | xcpretty --simple --color --report junit && exit ${PIPESTATUS[0]}
27 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/Internal/Validator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | class Validator: ObservableObject {
11 | enum State: Equatable {
12 | case normal
13 | case error(message: String?)
14 | }
15 |
16 | @Published var state: State
17 | var value: Binding
18 |
19 | private let validator: FieldValidator
20 |
21 | init(using validator: @escaping FieldValidator) {
22 | self.value = .constant("")
23 | self.state = .normal
24 | self.validator = validator
25 | if !self.value.wrappedValue.isEmpty {
26 | self.validate()
27 | }
28 | }
29 |
30 | @discardableResult
31 | func validate() -> Bool {
32 | if let error = validator(value.wrappedValue) {
33 | state = .error(message: error.description)
34 | return false
35 | }
36 | state = .normal
37 | return true
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "AmplifyUIAuthenticator",
8 | defaultLocalization: "en",
9 | platforms: [.iOS(.v15), .macOS(.v12)],
10 | products: [
11 | .library(
12 | name: "Authenticator",
13 | targets: ["Authenticator"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/aws-amplify/amplify-swift", from: "2.45.0")
17 | ],
18 | targets: [
19 | .target(
20 | name: "Authenticator",
21 | dependencies: [
22 | .product(name: "Amplify", package: "amplify-swift"),
23 | .product(name: "AWSCognitoAuthPlugin", package: "amplify-swift")
24 | ],
25 | resources: [
26 | .process("Resources")
27 | ]),
28 | .testTarget(
29 | name: "AuthenticatorTests",
30 | dependencies: ["Authenticator"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/SignedInStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class SignedInStateTests: XCTestCase {
13 | private var state: SignedInState!
14 | private var authenticationService: MockAuthenticationService!
15 |
16 | override func setUp() {
17 | authenticationService = MockAuthenticationService()
18 | state = SignedInState(
19 | user: MockAuthenticationService.User(username: "username", userId: "userId"),
20 | authenticationService: authenticationService
21 | )
22 | }
23 |
24 | override func tearDown() {
25 | state = nil
26 | authenticationService = nil
27 | }
28 |
29 | func testSignOut() async {
30 | let result = await state.signOut()
31 | XCTAssertEqual(authenticationService.signOutCount, 1)
32 | XCTAssertTrue(result is MockAuthenticationService.SignOutResult)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ContinueSignInWithTOTPCopyKeyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Default QRCodeContent for the ``ContinueSignInWithTOTPSetupView``. It displays the view's QR Code
11 | public struct ContinueSignInWithTOTPCopyKeyView: View {
12 |
13 | @ObservedObject private var state: ContinueSignInWithTOTPSetupState
14 |
15 | public init(state: ContinueSignInWithTOTPSetupState) {
16 | self.state = state
17 | }
18 |
19 | public var body: some View {
20 | Button("authenticator.continueSignInWithTOTPSetup.button.copyKey".localized()) {
21 | #if os(iOS)
22 | UIPasteboard.general.string = state.sharedSecret
23 | #elseif os(macOS)
24 | let pasteboard = NSPasteboard.general
25 | pasteboard.clearContents()
26 | pasteboard.setString(state.sharedSecret, forType: .string)
27 | #endif
28 | }
29 | .buttonStyle(.capsule)
30 | }
31 |
32 | }
33 |
34 | extension ContinueSignInWithTOTPCopyKeyView: AuthenticatorLogging {}
35 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/SnapshotLogic/CleanCounterBetweenTestCases.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | let counterQueue = DispatchQueue(label: "com.amplify.authenticator.counter")
11 | var counterMap: [URL: Int] = [:]
12 |
13 | // We need to clean counter between tests executions in order to support test-iterations.
14 | class CleanCounterBetweenTestCases: NSObject, XCTestObservation {
15 | private static var registered = false
16 | private static var registerQueue = DispatchQueue(
17 | label: "com.amplify.authenticator.testObserver")
18 |
19 | static func registerIfNeeded() {
20 | registerQueue.sync {
21 | if !registered {
22 | registered = true
23 | XCTestObservationCenter.shared.addTestObserver(CleanCounterBetweenTestCases())
24 | }
25 | }
26 | }
27 |
28 | func testCaseDidFinish(_ testCase: XCTestCase) {
29 | counterQueue.sync {
30 | counterMap = [:]
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | DerivedData/
5 | .swiftpm/config/registries.json
6 | .netrc
7 |
8 | # Xcode setups
9 | /*.xcodeproj
10 | **/xcuserdata
11 | **/*.xcuserdata
12 |
13 | # Built assets #
14 | ######################
15 | .build/
16 | build
17 | builtFramework
18 | AWS*.framework
19 | node_modules
20 | package.json
21 | package-lock.json
22 |
23 | # Stuff that can't be committed #
24 | ######################
25 | *credentials.json
26 | __pycache__/
27 | *awsconfiguration.json
28 | *amplifyconfiguration.json
29 | awsconfiguration.json
30 | amplifyconfiguration.json
31 | credentials-mc.json
32 |
33 | # Amplify artifacts
34 | amplify/\#current-cloud-backend
35 | amplify/.config/local-*
36 | amplify/logs
37 | amplify/mock-data
38 | amplify/backend/amplify-meta.json
39 | amplify/backend/awscloudformation
40 | amplify/backend/.temp
41 | build/
42 | dist/
43 | node_modules/
44 | aws-exports.js
45 | awsconfiguration.json
46 | amplifyconfiguration.json
47 | amplifyconfiguration.dart
48 | amplify-build-config.json
49 | amplify-gradle-config.json
50 | amplifytools.xcconfig
51 | .secret-*
52 | Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
53 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/Internal/AuthenticatorStateProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import Foundation
10 |
11 | protocol AuthenticatorStateProtocol {
12 | var authenticationService: AuthenticationService { get }
13 | var configuration: CognitoConfiguration { get }
14 | var authenticationFlow: AuthenticationFlow { get }
15 | var step: Step { get }
16 | func setCurrentStep(_ step: Step)
17 | func move(to initialStep: AuthenticatorInitialStep)
18 | }
19 |
20 | extension AuthenticatorStateProtocol where Self == EmptyAuthenticatorState {
21 | static var empty: AuthenticatorStateProtocol {
22 | return EmptyAuthenticatorState()
23 | }
24 | }
25 |
26 | struct EmptyAuthenticatorState: AuthenticatorStateProtocol {
27 | var authenticationService: AuthenticationService = .default
28 | var configuration: CognitoConfiguration = .empty
29 | var authenticationFlow: AuthenticationFlow = .password
30 | var step: Step = .loading
31 | func setCurrentStep(_ step: Step) {}
32 | func move(to initialStep: AuthenticatorInitialStep) {}
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/SignedInState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import Foundation
10 |
11 | /// The state that represents that the ``Authenticator`` is in ``AuthenticatorStep/signedIn`` step
12 | /// It can be retrieved through `@EnvironmentObject var state: SignedInState`, but it will only be set once the Authenticator successfuly completes an authentication flow.
13 | public class SignedInState: ObservableObject {
14 | /// The signed in user
15 | public let user: AuthUser
16 | let authenticationService: AuthenticationService
17 |
18 | init(user: AuthUser, authenticationService: AuthenticationService) {
19 | self.user = user
20 | self.authenticationService = authenticationService
21 | }
22 |
23 | /// Performs a sign out.
24 | /// - Returns: A `AuthSignOutResult`
25 | @discardableResult public func signOut() async -> AuthSignOutResult {
26 | let result = await authenticationService.signOut(options: nil)
27 | log.verbose("Sign out result is \(result)")
28 | return result
29 | }
30 | }
31 |
32 | extension SignedInState: AuthenticatorLogging {}
33 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/PasskeyPrompt.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 |
10 | /// Represents when to prompt users to create a passkey
11 | public enum PasskeyPrompt: Equatable {
12 | /// Never prompt the user to create a passkey
13 | case never
14 |
15 | /// Always prompt the user to create a passkey
16 | case always
17 | }
18 |
19 | extension PasskeyPrompt: Codable {}
20 |
21 | /// Configuration for when to prompt users to create passkeys
22 | public struct PasskeyPrompts: Equatable {
23 | /// When to prompt after sign up
24 | public let afterSignUp: PasskeyPrompt
25 |
26 | /// When to prompt after sign in
27 | public let afterSignIn: PasskeyPrompt
28 |
29 | /// Creates a PasskeyPrompts configuration
30 | /// - Parameters:
31 | /// - afterSignUp: When to prompt after sign up. Defaults to `.always`
32 | /// - afterSignIn: When to prompt after sign in. Defaults to `.always`
33 | public init(afterSignUp: PasskeyPrompt = .always, afterSignIn: PasskeyPrompt = .always) {
34 | self.afterSignUp = afterSignUp
35 | self.afterSignIn = afterSignIn
36 | }
37 | }
38 |
39 | extension PasskeyPrompts: Codable {}
40 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/Mocks/MockAuthenticatorState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Foundation
9 | @testable import Authenticator
10 |
11 | class MockAuthenticatorState: AuthenticatorStateProtocol {
12 | var authenticationService: AuthenticationService = MockAuthenticationService()
13 |
14 | var authenticationFlow: AuthenticationFlow = .password
15 |
16 | var configuration = CognitoConfiguration(
17 | usernameAttributes: [],
18 | signupAttributes: [],
19 | passwordProtectionSettings: .init(minLength: 0, characterPolicy: []),
20 | verificationMechanisms: [],
21 | hasUserPool: true,
22 | hasIdentityPool: true
23 | )
24 |
25 | var setCurrentStepCount = 0
26 | var setCurrentStepValue: Step?
27 | func setCurrentStep(_ step: Step) {
28 | setCurrentStepCount += 1
29 | setCurrentStepValue = step
30 | }
31 |
32 | var mockedStep: Step?
33 | var step: Step {
34 | return mockedStep ?? .loading
35 | }
36 |
37 | var moveToCount = 0
38 | var moveToValue: AuthenticatorInitialStep?
39 | func move(to initialStep: AuthenticatorInitialStep) {
40 | moveToCount += 1
41 | moveToValue = initialStep
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorUITestUtils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProcessArgument.swift
3 | // AuthenticatorHostApp
4 | //
5 | // Created by Singh, Harshdeep on 2023-09-22.
6 | //
7 |
8 | import Foundation
9 | @testable import Authenticator
10 |
11 | let UITestKeyKey = "-uiTestArgsData"
12 |
13 | enum ProcessArgument: Codable {
14 | case hidesSignUpButton(Bool)
15 | case initialStep(AuthenticatorInitialStep)
16 | case authSignInStep(AuthUITestSignInStep)
17 | case userAttributes([UserAttribute])
18 | case passwordlessFlow(Bool)
19 | }
20 |
21 | enum UserAttribute: String, Codable {
22 | case username = "USERNAME"
23 | case email = "EMAIL"
24 | case phoneNumber = "PHONE_NUMBER"
25 | }
26 |
27 | public enum AuthUITestSignInStep: Codable {
28 | case confirmSignInWithSMSMFACode
29 | case confirmSignInWithCustomChallenge
30 | case confirmSignInWithNewPassword
31 | case confirmSignInWithTOTPCode
32 | case continueSignInWithTOTPSetup
33 | case continueSignInWithMFASelection
34 | case continueSignInWithMFASetupSelection
35 | case continueSignInWithEmailMFASetup
36 | case confirmSignInWithEmailMFACode
37 | case continueSignInWithFirstFactorSelection
38 | case confirmSignInWithOTP
39 | case confirmSignInWithPassword
40 | case resetPassword
41 | case confirmSignUp
42 | case done
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/AuthenticatorTextWithHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AuthenticatorTextWithHeader: View {
11 | @Environment(\.authenticatorTheme) private var theme
12 | private var title: String
13 | private var content: String
14 |
15 | init(title: String, content: String) {
16 | self.title = title
17 | self.content = content
18 | }
19 |
20 | var body: some View {
21 | VStack {
22 |
23 | SwiftUI.Text(title)
24 | .font(theme.fonts.headline)
25 | .foregroundColor(theme.colors.foreground.primary)
26 | .accessibilityAddTraits(.isStaticText)
27 | .multilineTextAlignment(.leading)
28 | .frame(maxWidth: .infinity, alignment: .leading)
29 |
30 | Spacer(minLength: 10)
31 |
32 | SwiftUI.Text(content)
33 | .font(theme.fonts.body)
34 | .foregroundColor(theme.colors.foreground.primary)
35 | .accessibilityAddTraits(.isStaticText)
36 | .multilineTextAlignment(.leading)
37 | .frame(maxWidth: .infinity, alignment: .leading)
38 |
39 | Spacer()
40 | }
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/AuthenticatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AuthenticatorView: View {
11 | @Environment(\.authenticatorTheme) private var theme
12 | @Environment(\.authenticatorOptions) private var options
13 | private var isBusy: Bool
14 | private let content: Content
15 |
16 | init(isBusy: Bool, @ViewBuilder content: () -> Content) {
17 | self.isBusy = isBusy
18 | self.content = content()
19 | }
20 |
21 | var body: some View {
22 | ZStack {
23 | ScrollView {
24 | VStack(spacing: theme.components.authenticator.spacing.vertical) {
25 | content
26 | }
27 | .padding(theme.components.authenticator.padding/2)
28 | .background(theme.components.authenticator.backgroundColor)
29 | .cornerRadius(theme.components.authenticator.cornerRadius)
30 | .padding(theme.components.authenticator.padding/2)
31 | }
32 | .blur(radius: isBusy ? options.busyStyle.blurRadius : 0)
33 | .disabled(isBusy)
34 |
35 | if isBusy {
36 | AnyView(options.busyStyle.content)
37 | }
38 | }
39 | .frame(maxWidth: .infinity, maxHeight: .infinity)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/DefaultHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct DefaultHeader: View {
11 | @Environment(\.authenticatorTheme) private var theme
12 | var title: String
13 | private var font: Font? = nil
14 | private var foregroundColor: Color? = nil
15 | private var alignment: Alignment = .leading
16 |
17 | init(title: String) {
18 | self.title = title
19 | }
20 |
21 | var body: some View {
22 | HStack {
23 | SwiftUI.Text(title)
24 | .frame(maxWidth: .infinity, alignment: alignment)
25 | .font(font ?? theme.fonts.title)
26 | .foregroundColor(foregroundColor ?? theme.colors.foreground.primary)
27 | .accessibilityAddTraits(.isHeader)
28 | Spacer()
29 | }
30 | }
31 |
32 | func font(_ font: Font) -> DefaultHeader {
33 | var view = self
34 | view.font = font
35 | return view
36 | }
37 |
38 | func foregroundColor(_ foregroundColor: Color) -> DefaultHeader {
39 | var view = self
40 | view.foregroundColor = foregroundColor
41 | return view
42 | }
43 |
44 | func alignment(_ alignment: Alignment) -> DefaultHeader {
45 | var view = self
46 | view.alignment = alignment
47 | return view
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amplify UI Authenticator for SwiftUI
2 |
3 | [](LICENSE)
4 | [](https://codecov.io/gh/aws-amplify/amplify-ui-swift-authenticator)
5 | [](https://discord.gg/jWVbPfC)
6 | [](https://github.com/aws-amplify/amplify-ui-swift-authenticator/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
7 | [](https://github.com/aws-amplify/amplify-ui-swift-authenticator/issues?q=is%3Aissue+label%3Afeature-request+is%3Aopen)
8 |
9 | The **Amplify UI Authenticator** is a component that supports several authentiation flows using [Amplify Authentication](https://docs.amplify.aws/lib/auth/getting-started/q/platform/ios/).
10 |
11 | More information on setting up and using the Authenticator is in the [Amplify UI Authenticator documentation](https://ui.docs.amplify.aws/swift/connected-components/authenticator).
12 |
13 | ## Security
14 |
15 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
16 |
17 | ## License
18 |
19 | This project is licensed under the Apache-2.0 License.
20 |
21 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/Color+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension Color {
11 | /// Creates a color using HSL (Hue, Saturation, and Lightness)
12 | init(hue: Int, saturation: Int, lightness: Int) {
13 | let hue = Double(hue) / 360.0
14 | let saturation = Double(saturation) / 100.0
15 | let lightness = Double(lightness) / 100.0
16 |
17 | let brightness = lightness + saturation * min(lightness, 1 - lightness)
18 | let newSaturation = brightness == 0 ? 0 : 2 * (1 - lightness / brightness)
19 |
20 | self.init(
21 | hue: hue,
22 | saturation: newSaturation,
23 | brightness: brightness
24 | )
25 | }
26 |
27 | /// Creates a color that suports Light and Dark mode
28 | init(light: Color, dark: Color) {
29 | #if os(iOS)
30 | self.init(
31 | uiColor: .init {
32 | if $0.userInterfaceStyle == .dark {
33 | return UIColor(dark)
34 | } else {
35 | return UIColor(light)
36 | }
37 | }
38 | )
39 | #elseif os(macOS)
40 | self.init(
41 | nsColor: .init(name: nil) {
42 | if $0.name == .darkAqua {
43 | return NSColor(dark)
44 | } else {
45 | return NSColor(light)
46 | }
47 | }
48 | )
49 | #endif
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/issue_closed.yml:
--------------------------------------------------------------------------------
1 | name: Issue Closed
2 |
3 | on:
4 | issues:
5 | types: [closed]
6 |
7 | permissions:
8 | issues: write
9 |
10 | jobs:
11 | cleanup-labels:
12 | runs-on: ubuntu-latest
13 | if: ${{ contains(github.event.issue.labels.*.name, 'pending-community-response') || contains(github.event.issue.labels.*.name, 'pending-maintainer-response') || contains(github.event.issue.labels.*.name, 'closing soon') || contains(github.event.issue.labels.*.name, 'pending-release') || contains(github.event.issue.labels.*.name, 'pending-triage') }}
14 | steps:
15 | - name: Remove unnecessary labels after closing
16 | shell: bash
17 | env:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 | ISSUE_NUMBER: ${{ github.event.issue.number }}
20 | REPOSITORY_NAME: ${{ github.event.repository.full_name }}
21 | run: |
22 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "closing soon" --remove-label "pending-community-response" --remove-label "pending-maintainer-response" --remove-label "pending-release" --remove-label "pending-triage"
23 |
24 | comment-visibility-warning:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: aws-actions/closed-issue-message@v1
28 | with:
29 | repo-token: "${{ secrets.GITHUB_TOKEN }}"
30 | message: |
31 | This issue is now closed. Comments on closed issues are hard for our team to see.
32 | If you need more assistance, please open a new issue that references this one.
33 | If you wish to keep having a conversation with other community members under this issue feel free to do so.
34 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Primitives/RadioButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This represents a simple Button with a Radio Button-like look.
11 | /// It automatically toggles its state on tap
12 | struct RadioButton: View {
13 | @Environment(\.authenticatorTheme) var theme
14 | @Binding private var isSelected: Bool
15 | private let label: String
16 | private let action: () -> ()
17 |
18 | init(
19 | label: String,
20 | isSelected: Binding,
21 | action: @escaping () -> ()
22 | ) {
23 | self.label = label
24 | self._isSelected = isSelected
25 | self.action = action
26 | }
27 |
28 | var body: some View {
29 | SwiftUI.Button(
30 | action: {
31 | isSelected.toggle()
32 | action()
33 | },
34 | label: {
35 | HStack(alignment: .center) {
36 | SwiftUI.Image(
37 | systemName: isSelected
38 | ? "circle.inset.filled"
39 | : "circle"
40 | )
41 | .font(.system(size: 24))
42 | .foregroundColor(foregroundColor)
43 | Text(label)
44 | .foregroundColor(theme.colors.foreground.primary)
45 | Spacer()
46 | }
47 | }
48 | )
49 | }
50 |
51 | private var foregroundColor: Color {
52 | if isSelected {
53 | return theme.colors.background.interactive
54 | }
55 |
56 | return theme.colors.border.primary
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.github/workflows/unit_tests.yml:
--------------------------------------------------------------------------------
1 | name: Run Unit Tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | unit-test-ios:
14 | runs-on: macos-15
15 | steps:
16 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
17 | - name: Unit test Authenticator on iOS
18 | run: xcodebuild test -scheme Authenticator -sdk 'iphonesimulator' -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' -derivedDataPath Build/ -enableCodeCoverage YES -clonedSourcePackagesDirPath ~/Library/Developer/Xcode/DerivedData/Authenticator | xcpretty --simple --color --report junit && exit ${PIPESTATUS[0]}
19 | - name: Generate Coverage Report
20 | continue-on-error: true
21 | run: |
22 | cd Build/Build/ProfileData
23 | cd $(ls -d */|head -n 1)
24 | pathCoverage=Build/Build/ProfileData/${PWD##*/}/Coverage.profdata
25 | cd ${{ github.workspace }}
26 | xcrun llvm-cov export -format="lcov" -instr-profile $pathCoverage Build/Build/Products/Debug-iphonesimulator/Authenticator.o > Authenticator-Coverage.lcov
27 | - name: Upload Report
28 | uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # v4.3.0
29 | with:
30 | flags: Authenticator
31 | token: ${{ secrets.CODECOV_TOKEN }}
32 |
33 | unit-test-macos:
34 | runs-on: macos-latest
35 | steps:
36 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
37 | - name: Unit test Authenticator on macOS
38 | run: xcodebuild test -scheme Authenticator -sdk 'macosx' -destination 'platform=macOS,arch=x86_64' | xcpretty --simple --color --report junit && exit ${PIPESTATUS[0]}
--------------------------------------------------------------------------------
/Sources/Authenticator/Utilities/Padding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | func padding(_ padding: AuthenticatorTheme.Padding?) -> some View {
12 | modifier(PaddingModifier(padding: padding))
13 | }
14 |
15 | func padding(_ edges: [Edge], _ padding: AuthenticatorTheme.Padding?) -> some View {
16 | modifier(PaddingWithEdgesModifier(edges: edges, padding: padding))
17 | }
18 |
19 | @ViewBuilder fileprivate func padding(_ edges: Edge.Set, _ padding: CGFloat?, if condition: Bool) -> some View {
20 | if condition {
21 | self.padding(edges, padding)
22 | } else {
23 | self
24 | }
25 | }
26 | }
27 |
28 | private struct PaddingModifier: ViewModifier {
29 | var padding: AuthenticatorTheme.Padding?
30 |
31 | func body(content: Content) -> some View {
32 | content
33 | .padding([.top], padding?.top)
34 | .padding([.bottom], padding?.bottom)
35 | .padding([.leading], padding?.leading)
36 | .padding([.trailing], padding?.trailing)
37 | }
38 | }
39 |
40 | private struct PaddingWithEdgesModifier: ViewModifier {
41 | var edges: [Edge]
42 | var padding: AuthenticatorTheme.Padding?
43 |
44 | func body(content: Content) -> some View {
45 | content
46 | .padding([.top], padding?.top, if: edges.contains(.top))
47 | .padding([.bottom], padding?.bottom, if: edges.contains(.bottom))
48 | .padding([.leading], padding?.leading, if: edges.contains(.leading))
49 | .padding([.trailing], padding?.trailing, if: edges.contains(.trailing))
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Extensions/EnvironmentValues+Authenticator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension EnvironmentValues {
11 | /// The Authenticator's ``AuthenticatorState``
12 | public var authenticatorState: AuthenticatorState {
13 | get {
14 | self[AuthenticatorStateKey.self]
15 | }
16 |
17 | set {
18 | self[AuthenticatorStateKey.self] = newValue
19 | }
20 | }
21 | }
22 |
23 | extension EnvironmentValues {
24 | var authenticatorTheme: AuthenticatorTheme {
25 | get {
26 | self[AuthenticationThemeKey.self]
27 | }
28 |
29 | set {
30 | self[AuthenticationThemeKey.self] = newValue
31 | }
32 | }
33 |
34 | var authenticationService: AuthenticationService {
35 | get {
36 | self[AuthenticationServiceKey.self]
37 | }
38 |
39 | set {
40 | self[AuthenticationServiceKey.self] = newValue
41 | }
42 | }
43 |
44 | var authenticatorOptions: AuthenticatorOptions {
45 | get {
46 | self[AuthenticatorOptionsKey.self]
47 | }
48 |
49 | set {
50 | self[AuthenticatorOptionsKey.self] = newValue
51 | }
52 | }
53 | }
54 |
55 | private struct AuthenticatorStateKey: EnvironmentKey {
56 | static let defaultValue: AuthenticatorState = .init()
57 | }
58 |
59 | private struct AuthenticationThemeKey: EnvironmentKey {
60 | static let defaultValue: AuthenticatorTheme = AuthenticatorTheme()
61 | }
62 |
63 | private struct AuthenticationServiceKey: EnvironmentKey {
64 | static let defaultValue: AuthenticationService = .default
65 | }
66 |
67 | private struct AuthenticatorOptionsKey: EnvironmentKey {
68 | static let defaultValue: AuthenticatorOptions = .init()
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ContinueSignInWithTOTPSetupQRCodeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Default QRCodeContent for the ``ContinueSignInWithTOTPSetupView``. It displays the view's QR Code
11 | public struct ContinueSignInWithTOTPSetupQRCodeView: View {
12 |
13 | @Environment(\.authenticatorTheme) private var theme
14 | @ObservedObject private var state: ContinueSignInWithTOTPSetupState
15 |
16 | public init(state: ContinueSignInWithTOTPSetupState) {
17 | self.state = state
18 | }
19 |
20 | public var body: some View {
21 | if let qrCodeImage = generateQRCode(qrCodeURIString: state.setupURI) {
22 | Image(decorative: qrCodeImage, scale: 1)
23 | .interpolation(.none)
24 | .resizable()
25 | .scaledToFit()
26 | .frame(width: theme.components.authenticator.qrCodeSize,
27 | height: theme.components.authenticator.qrCodeSize)
28 | }
29 | }
30 |
31 | private func generateQRCode(qrCodeURIString: String?) -> CGImage? {
32 | guard let qrCodeURIString = qrCodeURIString else {
33 | return nil
34 | }
35 |
36 | let filter = CIFilter.qrCodeGenerator()
37 | filter.message = Data(qrCodeURIString.utf8)
38 | guard let outputImage = filter.outputImage else {
39 | log.error("Unable to create a CI Image for TOTP Setup QRCode")
40 | return nil
41 | }
42 | guard let cgImage = CIContext().createCGImage(outputImage, from: outputImage.extent) else {
43 | log.error("Unable to create a CGImage from CIImage for TOTP Setup QRCode ")
44 | return nil
45 | }
46 | return cgImage
47 | }
48 | }
49 |
50 | extension ContinueSignInWithTOTPSetupQRCodeView: AuthenticatorLogging {}
51 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ContinueSignInWithEmailMFASetupState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Continue Sign In with Email MFA Setup Challenge, representing the ``Authenticator`` is in the ``AuthenticatorStep/continueSignInWithEmailMFASetup`` step.
13 | public class ContinueSignInWithEmailMFASetupState: AuthenticatorBaseState {
14 | /// The email provided by the user
15 | @Published public var email: String = ""
16 |
17 | override init(credentials: Credentials) {
18 | super.init(credentials: credentials)
19 | }
20 |
21 | init(authenticatorState: AuthenticatorStateProtocol) {
22 | super.init(authenticatorState: authenticatorState,
23 | credentials: Credentials())
24 | }
25 |
26 | /// Attempts to continue user's sign by providing email.
27 | ///
28 | /// Automatically sets the Authenticator's next step accordingly, as well as the
29 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
30 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
31 | public func continueSignIn() async throws {
32 | setBusy(true)
33 |
34 | do {
35 | log.verbose("Attempting to continue Sign In with Email setup")
36 | let result = try await authenticationService.confirmSignIn(
37 | challengeResponse: email,
38 | options: nil
39 | )
40 | let nextStep = try await nextStep(for: result)
41 |
42 | setBusy(false)
43 |
44 | authenticatorState.setCurrentStep(nextStep)
45 | } catch {
46 | log.error("Continue Sign In with Email MFA Setup failed")
47 | setBusy(false)
48 | let authenticationError = self.error(for: error)
49 | setMessage(authenticationError)
50 | throw authenticationError
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ConfirmSignInWithNewPasswordState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Confirm Sign In with New Password content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithNewPassword`` step.
13 | public class ConfirmSignInWithNewPasswordState: AuthenticatorBaseState {
14 | /// The new password provided by the user
15 | @Published public var newPassword: String = ""
16 |
17 | /// The new password confirmation provided by the user
18 | @Published public var confirmPassword: String = ""
19 |
20 | /// Attempts to confirm the user's Sign In with the provided new password
21 | ///
22 | /// Automatically sets the Authenticator's next step accordingly, as well as the
23 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
24 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
25 | public func confirmSignIn() async throws {
26 | setBusy(true)
27 |
28 | do {
29 | log.verbose("Attempting to confirm Sign In")
30 | let result = try await authenticationService.confirmSignIn(
31 | challengeResponse: newPassword,
32 | options: nil
33 | )
34 |
35 | let nextStep = try await nextStep(for: result)
36 |
37 | setBusy(false)
38 |
39 | authenticatorState.setCurrentStep(nextStep)
40 | } catch {
41 | log.error("Confirm Sign In with new password failed")
42 | setBusy(false)
43 | let authenticationError = self.error(for: error)
44 | setMessage(authenticationError)
45 | throw authenticationError
46 | }
47 | }
48 | }
49 |
50 | extension ConfirmSignInWithNewPasswordState {
51 | enum Field: Int, Hashable, CaseIterable {
52 | case newPassword
53 | case newPasswordConfirmation
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Primitives/ImageButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This is a convenient way of having a Button display a single known image
11 | struct ImageButton: View {
12 | private let image: Image
13 | private let action: () -> ()
14 | private var color: Color?
15 |
16 | init(
17 | _ image: Image,
18 | _ action: @escaping () ->()
19 | ) {
20 | self.image = image
21 | self.action = action
22 | }
23 |
24 | var body: some View {
25 | SwiftUI.Button(
26 | action: {
27 | action()
28 | },
29 | label: {
30 | SwiftUI.Image(systemName: image.rawValue)
31 | }
32 | )
33 | .buttonStyle(.plain)
34 | .foregroundColor(color)
35 | .accessibilityLabel(
36 | Text(accessibilityLabel)
37 | )
38 | }
39 |
40 | func tintColor(_ color: Color?) -> Self {
41 | var view = self
42 | view.color = color
43 | return view
44 | }
45 |
46 | private var accessibilityLabel: String {
47 | switch image {
48 | case .close:
49 | return "authenticator.imageButton.close".localized()
50 | case .clear:
51 | return "authenticator.imageButton.clear".localized()
52 | case .open:
53 | return "authenticator.imageButton.open".localized()
54 | case .showPassword:
55 | return "authenticator.imageButton.showPassword".localized()
56 | case .hidePassword:
57 | return "authenticator.imageButton.hidePassword".localized()
58 | case .calendar:
59 | return "authenticator.imageButton.calendar".localized()
60 | }
61 | }
62 | }
63 |
64 | extension ImageButton {
65 | enum Image: String {
66 | case close = "x.circle.fill"
67 | case clear = "xmark.circle.fill"
68 | case open = "chevron.down.circle"
69 | case showPassword = "eye.fill"
70 | case hidePassword = "eye.slash.fill"
71 | case calendar = "calendar"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp/Utils/AuthCategoryConfigurationFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | @_spi(InternalAmplifyConfiguration)
11 | @testable import AWSCognitoAuthPlugin
12 | import Foundation
13 |
14 | class AuthCategoryConfigurationFactory {
15 | static var shared = AuthCategoryConfigurationFactory()
16 |
17 | private var usernameAttributes: [JSONValue] = []
18 | private var signupAttributes: [JSONValue] = []
19 | private var verificationMechanisms: [JSONValue] = [
20 | .string("EMAIL")
21 | ]
22 |
23 | func createConfiguration() -> AuthCategoryConfiguration {
24 | return AuthCategoryConfiguration(plugins: [
25 | "awsCognitoAuthPlugin": [
26 | "CognitoUserPool": [
27 | "Default": [
28 | "PoolId": "PoolId",
29 | "AppClientId": "AppClientId",
30 | "Region": "us-east-1"
31 | ]
32 | ],
33 | "CredentialsProvider": [
34 | "CognitoIdentity": [
35 | "Default": [
36 | "PoolId": "PoolId",
37 | "Region": "us-east-1"
38 | ]
39 | ]
40 | ],
41 | "Auth": [
42 | "Default": [
43 | "usernameAttributes": .array(usernameAttributes),
44 | "signupAttributes": .array(signupAttributes),
45 | "verificationMechanisms": .array(verificationMechanisms),
46 | "passwordProtectionSettings": [
47 | "passwordPolicyMinLength": 8,
48 | "passwordPolicyCharacters": []
49 | ]
50 | ]
51 | ]
52 | ]
53 | ])
54 | }
55 |
56 | func setUserAtributes(_ userAttributesArg: [UserAttribute]) {
57 | usernameAttributes = userAttributesArg.map({ .string($0.rawValue) })
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.github/workflows/issue_comment.yml:
--------------------------------------------------------------------------------
1 | name: Issue Comment
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | jobs:
8 | notify:
9 | runs-on: ubuntu-latest
10 | permissions: {}
11 | if: ${{ !github.event.issue.pull_request && !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }}
12 | steps:
13 | - name: Run webhook curl command
14 | env:
15 | WEBHOOK_URL: ${{ secrets.SLACK_COMMENT_WEBHOOK_URL }}
16 | COMMENT: ${{toJson(github.event.comment.body)}}
17 | USER: ${{github.event.comment.user.login}}
18 | COMMENT_URL: ${{github.event.comment.html_url}}
19 | shell: bash
20 | run: echo $COMMENT | sed "s/\\\n/. /g; s/\\\r//g; s/[^a-zA-Z0-9 &().,:]//g" | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"comment":"{}", "commentUrl":"'$COMMENT_URL'", "user":"'$USER'"}'
21 |
22 | adjust-labels:
23 | runs-on: ubuntu-latest
24 | if: ${{ github.event.issue.state == 'open' }}
25 | permissions:
26 | issues: write
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | ISSUE_NUMBER: ${{ github.event.issue.number }}
30 | REPOSITORY_NAME: ${{ github.event.repository.full_name }}
31 | steps:
32 | - name: remove pending-community-response when new comment received
33 | if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && !github.event.issue.pull_request }}
34 | shell: bash
35 | run: |
36 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-community-response"
37 | - name: add pending-maintainer-response when new community comment received
38 | if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }}
39 | shell: bash
40 | run: |
41 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response"
42 | - name: remove pending-maintainer-response when new owner/member comment received
43 | if: ${{ contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }}
44 | shell: bash
45 | run: |
46 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-maintainer-response"
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ContinueSignInWithMFASelectionState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Continue Sign In With MFA Selection content views, representing the ``Authenticator`` is in ``AuthenticatorStep/continueSignInWithMFASelection`` step.
13 | public class ContinueSignInWithMFASelectionState: AuthenticatorBaseState {
14 |
15 | /// The confirmation code provided by the user
16 | @Published public var selectedMFAType: MFAType?
17 |
18 | init(authenticatorState: AuthenticatorStateProtocol,
19 | allowedMFATypes: AllowedMFATypes) {
20 | self.allowedMFATypes = allowedMFATypes
21 | super.init(authenticatorState: authenticatorState,
22 | credentials: Credentials())
23 | }
24 |
25 | /// The `Amplify.AllowedMFATypes` associated with this state.
26 | public let allowedMFATypes: AllowedMFATypes
27 |
28 | /// Attempts to continue the user's sign in using the provided confirmation code.
29 | ///
30 | /// Automatically sets the Authenticator's next step accordingly, as well as the
31 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
32 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
33 | public func continueSignIn() async throws {
34 | guard let selectedMFAType = selectedMFAType else {
35 | log.error("MFA type not selected")
36 | return
37 | }
38 |
39 | setBusy(true)
40 | do {
41 | log.verbose("Attempting to confirm Sign In with Code")
42 | let result = try await authenticationService.confirmSignIn(
43 | challengeResponse: selectedMFAType.challengeResponse,
44 | options: nil
45 | )
46 | let nextStep = try await nextStep(for: result)
47 |
48 | setBusy(false)
49 |
50 | authenticatorState.setCurrentStep(nextStep)
51 | } catch {
52 | log.error("Confirm Sign In with Code failed")
53 | setBusy(false)
54 | let authenticationError = self.error(for: error)
55 | setMessage(authenticationError)
56 | throw authenticationError
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ResetPasswordState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Reset Password content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/resetPassword`` step.
12 | public class ResetPasswordState: AuthenticatorBaseState {
13 | /// The username provided by the user
14 | @Published public var username: String = "" {
15 | didSet {
16 | credentials.username = username
17 | }
18 | }
19 |
20 | /// Attempts to request a password reset for the provided username.
21 | ///
22 | /// Automatically sets the Authenticator's next step accordingly, as well as the
23 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
24 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
25 | public func resetPassword() async throws {
26 | setBusy(true)
27 | do {
28 | log.verbose("Attempting to reset password")
29 | let result = try await authenticationService.resetPassword(
30 | for: username,
31 | options: nil
32 | )
33 | setBusy(false)
34 |
35 | switch result.nextStep {
36 | case .confirmResetPasswordWithCode(let details, _):
37 | authenticatorState.setCurrentStep(.confirmResetPassword(deliveryDetails: details))
38 | case .done:
39 | // This should not happen, go back to Sign In screen
40 | log.warn("Received done next step after initiating a reset password. This is unexpected")
41 | authenticatorState.setCurrentStep(.signIn)
42 | }
43 | } catch {
44 | log.error("Unable to initialize a password reset")
45 | setBusy(false)
46 | let authenticationError = self.error(for: error)
47 | setMessage(authenticationError)
48 | throw authenticationError
49 | }
50 | }
51 |
52 | /// Manually moves the Authenticator to a different initial step
53 | /// - Parameter initialStep: The desired ``AuthenticatorInitialStep``
54 | public func move(to initialStep: AuthenticatorInitialStep) {
55 | authenticatorState.move(to: initialStep)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/.github/workflows/issue_opened.yml:
--------------------------------------------------------------------------------
1 | name: Issue Opened
2 | on:
3 | issues:
4 | types: [opened]
5 |
6 | jobs:
7 | notify:
8 | runs-on: ubuntu-latest
9 | permissions: {}
10 | if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }}
11 | steps:
12 | - name: Run webhook curl command
13 | env:
14 | WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }}
15 | ISSUE: ${{toJson(github.event.issue.title)}}
16 | ISSUE_URL: ${{github.event.issue.html_url}}
17 | USER: ${{github.event.issue.user.login}}
18 | shell: bash
19 | run: echo $ISSUE | sed 's/[^a-zA-Z0-9 &().,:]//g' | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"issue":"{}", "issueUrl":"'$ISSUE_URL'", "user":"'$USER'"}'
20 |
21 | add-issue-opened-labels:
22 | runs-on: ubuntu-latest
23 | permissions:
24 | issues: write
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | ISSUE_NUMBER: ${{ github.event.issue.number }}
28 | REPOSITORY_NAME: ${{ github.event.repository.full_name }}
29 | steps:
30 | - name: Add the pending-triage label
31 | shell: bash
32 | run: |
33 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-triage"
34 | - name: Add the pending-maintainer-response label
35 | if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }}
36 | shell: bash
37 | run: |
38 | gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response"
39 |
40 | maintainer-opened:
41 | runs-on: ubuntu-latest
42 | permissions:
43 | issues: write
44 | if: ${{ contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }}
45 | steps:
46 | - name: Post comment if maintainer opened.
47 | shell: bash
48 | env:
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | ISSUE_NUMBER: ${{ github.event.issue.number }}
51 | REPOSITORY_NAME: ${{ github.event.repository.full_name }}
52 | run: |
53 | gh issue comment $ISSUE_NUMBER --repo $REPOSITORY_NAME -b "This issue was opened by a maintainer of this repository; updates will be posted here. If you are also experiencing this issue, please comment here with any relevant information so that we're aware and can prioritize accordingly."
54 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ConfirmSignInWithCustomChallengeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step.
12 | public struct ConfirmSignInWithCustomChallengeView: View {
14 | @ObservedObject private var state: ConfirmSignInWithCodeState
15 | private let content: ConfirmSignInWithCodeView
16 |
17 | /// Creates a `ConfirmSignInWithCustomChallengeView`
18 | /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view
19 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithCustomChallengeHeader``
20 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to `SwiftUI.EmptyView`
21 | public init(
22 | state: ConfirmSignInWithCodeState,
23 | @ViewBuilder headerContent: () -> Header = {
24 | ConfirmSignInWithCustomChallengeHeader()
25 | },
26 | @ViewBuilder footerContent: () -> Footer = {
27 | EmptyView()
28 | }
29 | ) {
30 | self.state = state
31 | self.content = ConfirmSignInWithCodeView(
32 | state: state,
33 | headerContent: headerContent,
34 | footerContent: footerContent
35 | )
36 | }
37 |
38 | public var body: some View {
39 | content
40 | }
41 |
42 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
43 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
44 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
45 | state.errorTransform = errorTransform
46 | return self
47 | }
48 | }
49 |
50 | /// Default header for the ``ConfirmSignInWithCustomChallengeView``. It displays the view's title
51 | public struct ConfirmSignInWithCustomChallengeHeader: View {
52 | public init() {}
53 | public var body: some View {
54 | DefaultHeader(
55 | title: "authenticator.confirmSignInWithCustomChallenge.title".localized()
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ContinueSignInWithMFASetupSelectionState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Continue Sign In With MFA Setup Selection content views, representing the ``Authenticator`` is in the ``AuthenticatorStep/continueSignInWithMFASetupSelection`` step.
13 | public class ContinueSignInWithMFASetupSelectionState: AuthenticatorBaseState {
14 |
15 | /// The MFA type selected by the user
16 | @Published public var selectedMFATypeToSetup: MFAType?
17 |
18 | init(authenticatorState: AuthenticatorStateProtocol,
19 | allowedMFATypes: AllowedMFATypes) {
20 | self.allowedMFATypes = allowedMFATypes
21 | super.init(authenticatorState: authenticatorState,
22 | credentials: Credentials())
23 | }
24 |
25 | /// The `Amplify.AllowedMFATypes` associated with this state.
26 | public let allowedMFATypes: AllowedMFATypes
27 |
28 | /// Attempts to continue the user's sign in using the provided MFA type to setup.
29 | ///
30 | /// Automatically sets the Authenticator's next step accordingly, as well as the
31 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
32 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
33 | public func continueSignIn() async throws {
34 | guard let selectedMFATypeToSetup = selectedMFATypeToSetup else {
35 | log.error("MFA type not selected")
36 | return
37 | }
38 |
39 | setBusy(true)
40 | do {
41 | log.verbose("Attempting to continue Sign In with selected MFA type to setup")
42 | let result = try await authenticationService.confirmSignIn(
43 | challengeResponse: selectedMFATypeToSetup.challengeResponse,
44 | options: nil
45 | )
46 | let nextStep = try await nextStep(for: result)
47 |
48 | setBusy(false)
49 |
50 | authenticatorState.setCurrentStep(nextStep)
51 | } catch {
52 | log.error("Continue Sign In with MFA Setup Selection failed")
53 | setBusy(false)
54 | let authenticationError = self.error(for: error)
55 | setMessage(authenticationError)
56 | throw authenticationError
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/TestCases/PasskeyPromptTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import XCTest
9 |
10 | final class PasskeyPromptTests: AuthenticatorBaseTestCase {
11 |
12 | func testSignInPasskeyPrompt() throws {
13 | launchAppAndLogin(with: [
14 | .hidesSignUpButton(false),
15 | .initialStep(.signIn),
16 | .authSignInStep(.done)
17 | ])
18 | assertSnapshot()
19 | }
20 |
21 | func testSignUpPasskeyPrompt() throws {
22 |
23 | let app = XCUIApplication()
24 |
25 | launchApp(with: [
26 | .hidesSignUpButton(false),
27 | .initialStep(.signUp),
28 | .authSignInStep(.done),
29 | .passwordlessFlow(true)
30 | ])
31 |
32 | // Enter some username
33 | app.textFields.firstMatch.tap()
34 | app.textFields.firstMatch.typeText("username")
35 |
36 | // Enter some username
37 | app.textFields["Enter your email"].tap()
38 | app.textFields["Enter your email"].typeText("username@username.com")
39 |
40 | // Tap Sign in button
41 | app.buttons["Create account"].firstMatch.tap()
42 |
43 | // Wait for Sign In view to disappear
44 | let expectation = expectation(
45 | for: .init(format: "exists == false"),
46 | evaluatedWith: app.staticTexts["Create account"])
47 | let result = XCTWaiter.wait(for: [expectation], timeout: 5.0)
48 | XCTAssertEqual(result, .completed)
49 |
50 | assertSnapshot()
51 | }
52 |
53 | func testSignInPasskeyCreated() throws {
54 | launchAppAndLogin(with: [
55 | .hidesSignUpButton(false),
56 | .initialStep(.signIn),
57 | .authSignInStep(.done)
58 | ])
59 |
60 | let app = XCUIApplication()
61 | // Tap Sign in button
62 | app.buttons["Create a Passkey"].firstMatch.tap()
63 |
64 | // Wait for Sign In view to disappear
65 | let expectation = expectation(
66 | for: .init(format: "exists == false"),
67 | evaluatedWith: app.staticTexts["Create a Passkey"])
68 | let result = XCTWaiter.wait(for: [expectation], timeout: 5.0)
69 | XCTAssertEqual(result, .completed)
70 |
71 | assertSnapshot()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/AuthenticatorMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// A message that is displayed in the Authenticator
11 | public protocol AuthenticatorMessage {
12 | /// The message to display
13 | var content: String { get }
14 | /// The style in which this message is displayed
15 | var style: AuthenticatorMessageStyle { get }
16 | }
17 |
18 | /// The style used to display an ``AuthenticatorMessage``
19 | public struct AuthenticatorMessageStyle: Equatable {
20 | private enum InternalStyle {
21 | case info
22 | case error
23 | }
24 | private var type: InternalStyle
25 |
26 | private init(type: InternalStyle) {
27 | self.type = type
28 | }
29 |
30 | /// Used to display Information messages
31 | public static let info: AuthenticatorMessageStyle = .init(type: .info)
32 |
33 | /// Used to display error messages
34 | public static let error: AuthenticatorMessageStyle = .init(type: .error)
35 | }
36 |
37 | extension AuthenticatorMessage where Self == AuthenticatorInformation {
38 | /// A simple info message
39 | /// - Parameter message: The message that will be displayed
40 | public static func info(message: String) -> Self {
41 | return AuthenticatorInformation(content: message)
42 | }
43 | }
44 |
45 | extension AuthenticatorMessage where Self == AuthenticatorError {
46 | /// A simple error message
47 | /// - Parameter message: The message that will be displayed
48 | public static func error(message: String) -> Self {
49 | return AuthenticatorError(content: message)
50 | }
51 | }
52 |
53 | /// Represent a message that displays normal information
54 | public struct AuthenticatorInformation: AuthenticatorMessage {
55 | public let content: String
56 | public let style: AuthenticatorMessageStyle = .info
57 | }
58 |
59 | /// Represent a message that displays an error
60 | public struct AuthenticatorError: LocalizedError, AuthenticatorMessage {
61 | public let content: String
62 | public let style: AuthenticatorMessageStyle = .error
63 |
64 | /// An unknown error.
65 | public static func unknown(from error: Error) -> Self {
66 | log.verbose("Creating an unknown AuthenticatorError")
67 | log.verbose(error)
68 |
69 | return AuthenticatorError(
70 | content: "authenticator.unknownError".localized()
71 | )
72 | }
73 | }
74 |
75 | extension AuthenticatorError: AuthenticatorLogging {}
76 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Utilities/KeyboardIterableFields.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | protocol KeyboardIterableFields: AuthenticatorLogging {
11 | associatedtype Field: Hashable
12 | var focusedField: FocusState {set get}
13 |
14 | func focusPreviousField()
15 |
16 | func focusNextField()
17 |
18 | var hasPreviousField: Bool { get }
19 |
20 | var hasNextField: Bool { get }
21 | }
22 |
23 | extension KeyboardIterableFields where Field: RawRepresentable, Field: CaseIterable {
24 | private var currentIndex: Int? {
25 | return focusedField.wrappedValue?.rawValue
26 | }
27 |
28 | func focusPreviousField() {
29 | guard let currentIndex = currentIndex else { return }
30 | focusedField.wrappedValue = .init(rawValue: currentIndex - 1)
31 | }
32 |
33 | func focusNextField() {
34 | guard let currentIndex = currentIndex else { return }
35 | focusedField.wrappedValue = .init(rawValue: currentIndex + 1)
36 | }
37 |
38 | var hasPreviousField: Bool {
39 | guard let currentIndex = currentIndex else { return false }
40 | return currentIndex - 1 >= 0
41 | }
42 |
43 | var hasNextField: Bool {
44 | guard let currentIndex = currentIndex else { return false }
45 | return currentIndex + 1 < Field.allCases.count
46 | }
47 | }
48 |
49 | extension View {
50 | func keyboardIterableToolbar(fields: F) -> some View {
51 | self.modifier(KeyboardIterableToolbar(fields: fields))
52 | }
53 | }
54 |
55 | private struct KeyboardIterableToolbar: ViewModifier where V: KeyboardIterableFields {
56 | let fields: V
57 |
58 | func body(content: Content) -> some View {
59 | content
60 | .toolbar {
61 | SwiftUI.ToolbarItemGroup(placement: .keyboard) {
62 | SwiftUI.Button(action: fields.focusPreviousField) {
63 | Image(systemName: "chevron.up")
64 | }
65 | .disabled(!fields.hasPreviousField)
66 |
67 | SwiftUI.Button(action: fields.focusNextField) {
68 | Image(systemName: "chevron.down")
69 | }
70 | .disabled(!fields.hasNextField)
71 |
72 | Spacer()
73 |
74 | SwiftUI.Button("authenticator.keyboardToolbar.Done".localized()) {
75 | fields.focusedField.wrappedValue = nil
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/SignInConfirmPasswordState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Sign In Confirm Password content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/signInConfirmPassword`` step.
12 | public class SignInConfirmPasswordState: AuthenticatorBaseState {
13 | /// The password provided by the user
14 | @Published public var password: String = "" {
15 | didSet {
16 | credentials.password = password
17 | }
18 | }
19 |
20 | /// The username for this sign-in attempt
21 | public var username: String {
22 | return credentials.username
23 | }
24 |
25 | override init(credentials: Credentials) {
26 | super.init(credentials: credentials)
27 | }
28 |
29 | init(authenticatorState: AuthenticatorStateProtocol) {
30 | super.init(authenticatorState: authenticatorState,
31 | credentials: Credentials())
32 | }
33 |
34 | /// Attempts to confirm the password and complete sign-in
35 | ///
36 | /// Automatically sets the Authenticator's next step accordingly, as well as the
37 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
38 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
39 | public func confirmPassword() async throws {
40 | setBusy(true)
41 |
42 | do {
43 | log.verbose("Attempting to confirm Sign In with Password")
44 | let result = try await authenticationService.confirmSignIn(
45 | challengeResponse: password,
46 | options: nil
47 | )
48 | let nextStep = try await nextStep(for: result)
49 |
50 | setBusy(false)
51 |
52 | authenticatorState.setCurrentStep(nextStep)
53 | } catch {
54 | log.error("Confirm Sign In with Password failed")
55 | setBusy(false)
56 | let authenticationError = self.error(for: error)
57 | setMessage(authenticationError)
58 | throw authenticationError
59 | }
60 | }
61 |
62 | /// Manually moves the Authenticator to a different initial step
63 | /// - Parameter initialStep: The desired ``AuthenticatorInitialStep``
64 | public func move(to initialStep: AuthenticatorInitialStep) {
65 | authenticatorState.move(to: initialStep)
66 | }
67 | }
68 |
69 | extension SignInConfirmPasswordState {
70 | enum Field: Int, Hashable, CaseIterable {
71 | case password
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ConfirmSignInWithCodeState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Confirm Sign In with Custom Challenge and Confirm Sign In with MFA Code content views, representing the ``Authenticator`` is in either the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` or the ``AuthenticatorStep/confirmSignInWithMFACode`` step accordingly.
13 | public class ConfirmSignInWithCodeState: AuthenticatorBaseState {
14 | /// The confirmation code provided by the user
15 | @Published public var confirmationCode: String = ""
16 |
17 | override init(credentials: Credentials) {
18 | super.init(credentials: credentials)
19 | }
20 |
21 | init(authenticatorState: AuthenticatorStateProtocol) {
22 | super.init(authenticatorState: authenticatorState,
23 | credentials: Credentials())
24 | }
25 |
26 | /// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmSignInWithMFACode` or `confirmSignInWithOTP` step, it returns `nil`
27 | public var deliveryDetails: AuthCodeDeliveryDetails? {
28 | switch authenticatorState.step {
29 | case .confirmSignInWithMFACode(let deliveryDetails),
30 | .confirmSignInWithOTP(let deliveryDetails):
31 | return deliveryDetails
32 | default:
33 | return nil
34 | }
35 | }
36 |
37 | /// Attempts to confirm the user's sign in using the provided confirmation code.
38 | ///
39 | /// Automatically sets the Authenticator's next step accordingly, as well as the
40 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
41 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
42 | public func confirmSignIn() async throws {
43 | setBusy(true)
44 |
45 | do {
46 | log.verbose("Attempting to confirm Sign In with Code")
47 | let result = try await authenticationService.confirmSignIn(
48 | challengeResponse: confirmationCode,
49 | options: nil
50 | )
51 | let nextStep = try await nextStep(for: result)
52 |
53 | setBusy(false)
54 |
55 | authenticatorState.setCurrentStep(nextStep)
56 | } catch {
57 | log.error("Confirm Sign In with Code failed")
58 | setBusy(false)
59 | let authenticationError = self.error(for: error)
60 | setMessage(authenticationError)
61 | throw authenticationError
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostAppUITests/AuthenticatorBaseTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | // AuthenticatorHostAppUITests
4 | //
5 | // Created by Singh, Harshdeep on 2023-09-21.
6 | //
7 |
8 | import XCTest
9 |
10 | class AuthenticatorBaseTestCase: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | continueAfterFailure = false
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | XCUIApplication().terminate()
18 | }
19 |
20 | func assertSnapshot(
21 | named name: String? = nil,
22 | snapshotDirectory: String? = nil,
23 | timeout: TimeInterval = 5,
24 | file: StaticString = #file,
25 | testName: String = #function,
26 | line: UInt = #line
27 | ) {
28 | let result = Snapshotter.captureAndVerifySnapshot(
29 | for: XCUIApplication().screenshot().image,
30 | named: name,
31 | snapshotDirectory: snapshotDirectory,
32 | timeout: timeout,
33 | file: file,
34 | testName: testName,
35 | line: line)
36 |
37 | // Add the attachments to the test case
38 | result.attachments.forEach( { add($0) })
39 |
40 | XCTAssertTrue(
41 | result.didSucceed,
42 | "Snapshot Assertion failed for test. Description:\n\n\(result.message ?? "No description")")
43 | }
44 |
45 | func launchApp(with args: [ProcessArgument]) {
46 | // Launch Application
47 | let app = XCUIApplication()
48 |
49 | if let encodedData = try? JSONEncoder().encode(args),
50 | let stringJSON = String(data: encodedData, encoding: .utf8) {
51 | app.launchArguments = [
52 | UITestKeyKey, stringJSON,
53 | ]
54 | } else {
55 | print("Unable to encode process args")
56 | }
57 |
58 | app.launch()
59 | }
60 |
61 | func launchAppAndLogin(with args: [ProcessArgument], shouldEnterPassword: Bool = true) {
62 |
63 | // Launch Application
64 | launchApp(with: args)
65 | // Get app instance
66 | let app = XCUIApplication()
67 |
68 | // Enter some username
69 | app.textFields.firstMatch.tap()
70 | app.textFields.firstMatch.typeText("username")
71 |
72 | if shouldEnterPassword {
73 | // Enter some password
74 | app.secureTextFields.firstMatch.tap()
75 | app.secureTextFields.firstMatch.typeText("password")
76 | }
77 |
78 | // Tap Sign in button
79 | app.buttons["Sign In"].firstMatch.tap()
80 |
81 | // Wait for Sign In view to disappear
82 | let expectation = expectation(
83 | for: .init(format: "exists == false"),
84 | evaluatedWith: app.staticTexts["Sign In"])
85 | let result = XCTWaiter.wait(for: [expectation], timeout: 5.0)
86 | XCTAssertEqual(result, .completed)
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/PasskeyCreatedState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Passkey Created content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/passkeyCreated`` step.
12 | public class PasskeyCreatedState: AuthenticatorBaseState {
13 |
14 | /// The list of WebAuthn credentials (passkeys) for the user
15 | @Published public var passkeyCredentials: [AuthWebAuthnCredential] = []
16 |
17 | override init(credentials: Credentials) {
18 | super.init(credentials: credentials)
19 | }
20 |
21 | init(authenticatorState: AuthenticatorStateProtocol) {
22 | super.init(authenticatorState: authenticatorState,
23 | credentials: Credentials())
24 | }
25 |
26 | /// Fetches the list of passkey credentials for the user
27 | public func fetchPasskeyCredentials() async {
28 | do {
29 | log.verbose("Fetching passkey credentials")
30 |
31 | if #available(iOS 17.4, macOS 13.5, visionOS 1.0, *) {
32 | let result = try await authenticationService.listWebAuthnCredentials(options: nil)
33 |
34 | await MainActor.run {
35 | self.passkeyCredentials = result.credentials
36 | }
37 | log.verbose("Fetched \(result.credentials.count) passkey credentials")
38 | } else {
39 | log.error("WebAuthn is not supported on this platform (requires iOS 17.4+, macOS 13.5+, or visionOS 1.0+)")
40 | }
41 | } catch {
42 | log.error("Failed to fetch passkey credentials: \(error)")
43 | // Don't throw - just log the error, credentials list will remain empty
44 | }
45 | }
46 |
47 | /// Continues the authentication flow after passkey creation
48 | ///
49 | /// Automatically sets the Authenticator's next step accordingly, as well as the
50 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
51 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
52 | public func `continue`() async throws {
53 | setBusy(true)
54 |
55 | do {
56 | log.verbose("Continuing after passkey creation")
57 | // Use post-passkey logic (attribute verification and sign-in)
58 | let nextStep = try await nextStepAfterPasskeyFlow()
59 |
60 | setBusy(false)
61 | authenticatorState.setCurrentStep(nextStep)
62 | } catch {
63 | log.error("Continue after passkey creation failed")
64 | setBusy(false)
65 | let authenticationError = self.error(for: error)
66 | setMessage(authenticationError)
67 | throw authenticationError
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ConfirmSignInWithTOTPCodeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithTOTPCode`` step.
12 | public struct ConfirmSignInWithTOTPView: View {
14 | @Environment(\.authenticatorState) private var authenticatorState
15 | @ObservedObject private var state: ConfirmSignInWithCodeState
16 | private let content: ConfirmSignInWithCodeView
17 |
18 | /// Creates a `ConfirmSignInWithTOTPView`
19 | /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view
20 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithTOTPHeader``
21 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithTOTPFooter``
22 | public init(
23 | state: ConfirmSignInWithCodeState,
24 | @ViewBuilder headerContent: () -> Header = {
25 | ConfirmSignInWithTOTPHeader()
26 | },
27 | @ViewBuilder footerContent: () -> Footer = {
28 | ConfirmSignInWithTOTPFooter()
29 | }
30 | ) {
31 | self.state = state
32 | self.content = ConfirmSignInWithCodeView(
33 | state: state,
34 | headerContent: headerContent,
35 | footerContent: footerContent
36 | )
37 | }
38 |
39 | public var body: some View {
40 | content
41 | }
42 |
43 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
44 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
45 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
46 | state.errorTransform = errorTransform
47 | return self
48 | }
49 | }
50 |
51 | /// Default header for the ``ConfirmSignInWithTOTPCodeView``. It displays the view's title
52 | public struct ConfirmSignInWithTOTPHeader: View {
53 | public init() {}
54 | public var body: some View {
55 | DefaultHeader(
56 | title: "authenticator.confirmSignInWithCode.totp.title".localized()
57 | )
58 | }
59 | }
60 |
61 | /// Default footer for the ``ConfirmSignInWithTOTPCodeView``. It displays the "Back to Sign In" button
62 | public struct ConfirmSignInWithTOTPFooter: View {
63 | @Environment(\.authenticatorState) private var authenticatorState
64 |
65 | public init() {}
66 | public var body: some View {
67 | Button("authenticator.confirmSignInWithCode.totp.button.backToSignIn".localized()) {
68 | authenticatorState.move(to: .signIn)
69 | }
70 | .buttonStyle(.link)
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Authenticator.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
54 |
55 |
61 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ResetPasswordStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ResetPasswordStateTests: XCTestCase {
13 | private var state: ResetPasswordState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | state = ResetPasswordState(credentials: Credentials())
19 | authenticatorState = MockAuthenticatorState()
20 | authenticationService = MockAuthenticationService()
21 | authenticatorState.authenticationService = authenticationService
22 | state.configure(with: authenticatorState)
23 | }
24 |
25 | override func tearDown() {
26 | state = nil
27 | authenticatorState = nil
28 | authenticationService = nil
29 | }
30 |
31 | func testResetPassword_withConfirmReset_shouldSetNextStep() async throws {
32 | let destination = DeliveryDestination.sms("12345678")
33 | authenticationService.mockedResetPasswordResult = .init(
34 | isPasswordReset: false,
35 | nextStep: .confirmResetPasswordWithCode(.init(destination: destination), nil)
36 | )
37 |
38 | try await state.resetPassword()
39 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
40 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
41 | guard case .confirmResetPassword(let deliveryDetails) = currentStep else {
42 | XCTFail("Expected confirmResetPassword, was \(currentStep)")
43 | return
44 | }
45 | XCTAssertEqual(deliveryDetails?.destination, destination)
46 | }
47 |
48 | func testResetPassword_withDone_shouldSetNextStep() async throws {
49 | authenticationService.mockedResetPasswordResult = .init(
50 | isPasswordReset: true,
51 | nextStep: .done
52 | )
53 |
54 | try await state.resetPassword()
55 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
56 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
57 | guard case .signIn = currentStep else {
58 | XCTFail("Expected signIn, was \(currentStep)")
59 | return
60 | }
61 | }
62 |
63 | func testResetPassword_withFailure_shouldSetErrorMessage() async throws {
64 | do {
65 | try await state.resetPassword()
66 | XCTFail("Should not succeed")
67 | } catch {
68 | guard let authenticatorError = error as? AuthenticatorError else {
69 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
70 | return
71 | }
72 |
73 | let task = Task { @MainActor in
74 | XCTAssertNotNil(state.message)
75 | XCTAssertEqual(state.message?.content, authenticatorError.content)
76 | }
77 | await task.value
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ConfirmSignInWithOTPView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithOTP`` step.
12 | public struct ConfirmSignInWithOTPView: View {
14 | @Environment(\.authenticatorState) private var authenticatorState
15 | @ObservedObject private var state: ConfirmSignInWithCodeState
16 | private let content: ConfirmSignInWithCodeView
17 |
18 | /// Creates a `ConfirmSignInWithOTPView`
19 | /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view
20 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithOTPHeader``
21 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithOTPFooter``
22 | public init(
23 | state: ConfirmSignInWithCodeState,
24 | @ViewBuilder headerContent: () -> Header = {
25 | ConfirmSignInWithOTPHeader()
26 | },
27 | @ViewBuilder footerContent: () -> Footer = {
28 | ConfirmSignInWithOTPFooter()
29 | }
30 | ) {
31 | self.state = state
32 | self.content = ConfirmSignInWithCodeView(
33 | state: state,
34 | headerContent: headerContent,
35 | footerContent: footerContent
36 | )
37 | }
38 |
39 | public var body: some View {
40 | content
41 | .onAppear {
42 | state.message = .info(
43 | message: state.localizedMessage(for: state.deliveryDetails)
44 | )
45 | }
46 | }
47 |
48 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
49 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
50 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
51 | state.errorTransform = errorTransform
52 | return self
53 | }
54 | }
55 |
56 | /// Default header for the ``ConfirmSignInWithOTPView``. It displays the view's title
57 | public struct ConfirmSignInWithOTPHeader: View {
58 | public init() {}
59 | public var body: some View {
60 | DefaultHeader(
61 | title: "authenticator.confirmSignInWithOTP.title".localized()
62 | )
63 | }
64 | }
65 |
66 | /// Default footer for the ``ConfirmSignInWithOTPView``. It displays the "Back to Sign In" button
67 | public struct ConfirmSignInWithOTPFooter: View {
68 | @Environment(\.authenticatorState) private var authenticatorState
69 |
70 | public init() {}
71 | public var body: some View {
72 | Button("authenticator.confirmSignInWithCode.button.backToSignIn".localized()) {
73 | authenticatorState.move(to: .signIn)
74 | }
75 | .buttonStyle(.link)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/ConfirmSignInWithMFACodeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignInWithMFACode`` step.
12 | public struct ConfirmSignInWithMFACodeView: View {
14 | @Environment(\.authenticatorState) private var authenticatorState
15 | @ObservedObject private var state: ConfirmSignInWithCodeState
16 | private let content: ConfirmSignInWithCodeView
17 |
18 | /// Creates a `ConfirmSignInWithMFACodeView`
19 | /// - Parameter state: The ``ConfirmSignInWithCodeState`` that is observed by this view
20 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``ConfirmSignInWithMFACodeHeader``
21 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``ConfirmSignInWithMFACodeFooter``
22 | public init(
23 | state: ConfirmSignInWithCodeState,
24 | @ViewBuilder headerContent: () -> Header = {
25 | ConfirmSignInWithMFACodeHeader()
26 | },
27 | @ViewBuilder footerContent: () -> Footer = {
28 | ConfirmSignInWithMFACodeFooter()
29 | }
30 | ) {
31 | self.state = state
32 | self.content = ConfirmSignInWithCodeView(
33 | state: state,
34 | headerContent: headerContent,
35 | footerContent: footerContent
36 | )
37 | }
38 |
39 | public var body: some View {
40 | content
41 | .onAppear {
42 | state.message = .info(
43 | message: state.localizedMessage(for: state.deliveryDetails)
44 | )
45 | }
46 | }
47 |
48 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
49 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
50 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
51 | state.errorTransform = errorTransform
52 | return self
53 | }
54 | }
55 |
56 | /// Default header for the ``ConfirmSignInWithMFACodeView``. It displays the view's title
57 | public struct ConfirmSignInWithMFACodeHeader: View {
58 | public init() {}
59 | public var body: some View {
60 | DefaultHeader(
61 | title: "authenticator.confirmSignInWithMFACode.title".localized()
62 | )
63 | }
64 | }
65 |
66 | /// Default footer for the ``ConfirmSignInWithMFACodeView``. It displays the "Back to Sign In" button
67 | public struct ConfirmSignInWithMFACodeFooter: View {
68 | @Environment(\.authenticatorState) private var authenticatorState
69 |
70 | public init() {}
71 | public var body: some View {
72 | Button("authenticator.confirmSignInWithCode.button.backToSignIn".localized()) {
73 | authenticatorState.move(to: .signIn)
74 | }
75 | .buttonStyle(.link)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ConfirmResetPasswordState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Confirm Reset Password content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/confirmResetPassword`` step.
13 | public class ConfirmResetPasswordState: AuthenticatorBaseState {
14 | /// The confirmation code provided by the user
15 | @Published public var confirmationCode: String = ""
16 |
17 | /// The new password provided by the user
18 | @Published public var newPassword: String = ""
19 |
20 | /// The new password confirmation provided by the user
21 | @Published public var confirmPassword: String = ""
22 |
23 | /// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmResetPassword` step, it returns `nil`
24 | public var deliveryDetails: AuthCodeDeliveryDetails? {
25 | guard case .confirmResetPassword(let deliveryDetails) = authenticatorState.step else {
26 | return nil
27 | }
28 |
29 | return deliveryDetails
30 | }
31 |
32 | /// Attempts to confirm the new password using the provided values.
33 | ///
34 | /// Automatically sets the Authenticator's next step accordingly, as well as the
35 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
36 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
37 | public func confirmResetPassword() async throws {
38 | setBusy(true)
39 |
40 | do {
41 | log.verbose("Attempting to confirm Password Reset")
42 | try await authenticationService.confirmResetPassword(
43 | for: credentials.username,
44 | with: newPassword,
45 | confirmationCode: confirmationCode,
46 | options: nil
47 | )
48 |
49 | let nextStep = await nextStep()
50 | setBusy(false)
51 | authenticatorState.setCurrentStep(nextStep)
52 | } catch {
53 | log.error("Confirm Reset Password failed")
54 | setBusy(false)
55 | let authenticationError = self.error(for: error)
56 | setMessage(authenticationError)
57 | throw authenticationError
58 | }
59 | }
60 |
61 | private func nextStep() async -> Step {
62 | do {
63 | let result = try await authenticationService.signIn(
64 | username: credentials.username,
65 | password: newPassword,
66 | options: nil
67 | )
68 |
69 | return try await nextStep(for: result)
70 | } catch {
71 | log.error("Unable to Sign In after confirming password reset")
72 | log.error(error)
73 | return .signIn
74 | }
75 | }
76 | }
77 |
78 | extension ConfirmResetPasswordState {
79 | enum Field: Int, Hashable, CaseIterable {
80 | case confirmationCode
81 | case newPassword
82 | case newPasswordConfirmation
83 | }
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/VerifyUserState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Verify User content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/verifyUser`` step.
12 | public class VerifyUserState: AuthenticatorBaseState {
13 | /// The `Amplify.AuthUserAttributeKey` to be verified selected by the user
14 | @Published public var selectedField: AuthUserAttributeKey?
15 |
16 | /// An array of the unverified `Amplify.AuthUserAttributeKey` that are associated with this state.
17 | /// If the Authenticator is not in the `.verifyUser` step, it returns an empty array.
18 | public var unverifiedFields: [AuthUserAttributeKey] {
19 | guard case .verifyUser(let attributes) = authenticatorState.step else {
20 | return []
21 | }
22 |
23 | return attributes
24 | }
25 |
26 | /// Attempts to request a verification for the attribute selected by the user
27 | ///
28 | /// Automatically sets the Authenticator's next step accordingly, as well as the
29 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
30 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
31 | public func verifyUser() async throws {
32 | guard let key = selectedField else {
33 | return
34 | }
35 |
36 | setBusy(true)
37 |
38 | do {
39 | log.verbose("Attempting to verify user attribute \(key)")
40 | let result = try await authenticationService.sendVerificationCode(
41 | forUserAttributeKey: key,
42 | options: nil
43 | )
44 | setBusy(false)
45 | authenticatorState.setCurrentStep(
46 | .confirmVerifyUser(attribute: key, deliveryDetails: result)
47 | )
48 | } catch {
49 | log.error("Unable to send confirmation code for user attribute")
50 | let authenticationError = self.error(for: error)
51 | setMessage(authenticationError)
52 | throw authenticationError
53 | }
54 | }
55 |
56 | /// Skips the verification of any ``unverifiedFields`` and attempts to proceed with sign in
57 | ///
58 | /// Automatically sets the Authenticator's next step accordingly, as well as the
59 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
60 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
61 | public func skip() async throws {
62 | setBusy(true)
63 |
64 | do {
65 | log.verbose("Skipping user verification")
66 | let user = try await authenticatorState.authenticationService.getCurrentUser()
67 | authenticatorState.setCurrentStep(.signedIn(user: user))
68 | setBusy(false)
69 | } catch {
70 | log.error("Unable to get the current user after skipping verification")
71 | log.error(error)
72 | setBusy(false)
73 | // Go back to sign in
74 | authenticatorState.setCurrentStep(.signIn)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ConfirmSignUpState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 | import AWSCognitoAuthPlugin
11 |
12 | /// The state observed by the Confirm Sign Up content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/confirmSignUp`` step.
13 | public class ConfirmSignUpState: AuthenticatorBaseState {
14 | /// The confirmation code provided by the user
15 | @Published public var confirmationCode: String = ""
16 |
17 | /// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmSignUp` step, it returns `nil`
18 | public var deliveryDetails: AuthCodeDeliveryDetails? {
19 | guard case .confirmSignUp(let deliveryDetails) = authenticatorState.step else {
20 | return nil
21 | }
22 |
23 | return deliveryDetails
24 | }
25 |
26 | /// Attempts to confirm the user's Sign Up using the provided confirmation code.
27 | ///
28 | /// Automatically sets the Authenticator's next step accordingly, as well as the
29 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
30 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
31 | public func confirmSignUp() async throws {
32 | setBusy(true)
33 |
34 | do {
35 | log.verbose("Attempting to confirm Sign Up")
36 | let result = try await authenticationService.confirmSignUp(
37 | for: credentials.username,
38 | confirmationCode: confirmationCode,
39 | options: nil)
40 |
41 | let nextStep = try await nextStep(for: result)
42 | setBusy(false)
43 | authenticatorState.setCurrentStep(nextStep)
44 | } catch {
45 | log.error("Confirm Sign Up failed")
46 | setBusy(false)
47 | let authenticationError = self.error(for: error)
48 | setMessage(authenticationError)
49 | throw authenticationError
50 | }
51 | }
52 |
53 | /// Attempts to resend the user's Sign Up confirmation code.
54 | ///
55 | /// Automatically sets the Authenticator's next step accordingly, as well as the
56 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
57 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
58 | public func sendCode() async throws {
59 | setBusy(true)
60 |
61 | do {
62 | log.verbose("Attempting to resend the Sign Up code")
63 | let details = try await authenticationService.resendSignUpCode(
64 | for: credentials.username,
65 | options: nil
66 | )
67 |
68 | setMessage(.info(message: localizedMessage(for: details)))
69 | authenticatorState.setCurrentStep(.confirmSignUp(deliveryDetails: details))
70 | } catch {
71 | log.error("Unable to resend the Sign Up confirmation code")
72 | setBusy(false)
73 | let authenticationError = self.error(for: error)
74 | setMessage(authenticationError)
75 | throw authenticationError
76 | }
77 | }
78 |
79 | var username: String {
80 | return credentials.username
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ConfirmSignInWithNewPasswordStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ConfirmSignInWithNewPasswordStateTests: XCTestCase {
13 | private var state: ConfirmSignInWithNewPasswordState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | state = ConfirmSignInWithNewPasswordState(credentials: Credentials())
19 | authenticatorState = MockAuthenticatorState()
20 | authenticationService = MockAuthenticationService()
21 | authenticatorState.authenticationService = authenticationService
22 | state.configure(with: authenticatorState)
23 | }
24 |
25 | override func tearDown() {
26 | state = nil
27 | authenticatorState = nil
28 | authenticationService = nil
29 | }
30 |
31 | func testConfirmSignIn_withSuccess_shouldSetNextStep() async throws {
32 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .done)
33 | authenticationService.mockedCurrentUser = MockAuthenticationService.User(
34 | username: "username",
35 | userId: "userId"
36 | )
37 |
38 | try await state.confirmSignIn()
39 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
40 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
41 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
42 | guard case .signedIn(_) = currentStep else {
43 | XCTFail("Expected signedIn, was \(currentStep)")
44 | return
45 | }
46 | }
47 |
48 | func testConfirmSignIn_withError_shouldSetErrorMessage() async throws {
49 | do {
50 | try await state.confirmSignIn()
51 | XCTFail("Should not succeed")
52 | } catch {
53 | guard let authenticatorError = error as? AuthenticatorError else {
54 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
55 | return
56 | }
57 |
58 | let task = Task { @MainActor in
59 | XCTAssertNotNil(state.message)
60 | XCTAssertEqual(state.message?.content, authenticatorError.content)
61 | }
62 | await task.value
63 | }
64 | }
65 |
66 | func testConfirmSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws {
67 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .done)
68 | do {
69 | try await state.confirmSignIn()
70 | XCTFail("Should not succeed")
71 | } catch {
72 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
73 | guard let authenticatorError = error as? AuthenticatorError else {
74 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
75 | return
76 | }
77 |
78 | let task = Task { @MainActor in
79 | XCTAssertNotNil(state.message)
80 | XCTAssertEqual(state.message?.content, authenticatorError.content)
81 | }
82 | await task.value
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/PromptToCreatePasskeyState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Prompt To Create Passkey content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/promptToCreatePasskey`` step.
12 | public class PromptToCreatePasskeyState: AuthenticatorBaseState {
13 |
14 | override init(credentials: Credentials) {
15 | super.init(credentials: credentials)
16 | }
17 |
18 | init(authenticatorState: AuthenticatorStateProtocol) {
19 | super.init(authenticatorState: authenticatorState,
20 | credentials: Credentials())
21 | }
22 |
23 | /// Attempts to create a passkey for the user
24 | ///
25 | /// Automatically sets the Authenticator's next step accordingly, as well as the
26 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
27 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
28 | public func createPasskey() async throws {
29 | setBusy(true)
30 |
31 | do {
32 | log.verbose("Attempting to create passkey")
33 |
34 | // Call Amplify WebAuthn API to associate a passkey credential
35 | if #available(iOS 17.4, macOS 13.5, visionOS 1.0, *) {
36 | try await authenticationService.associateWebAuthnCredential(
37 | presentationAnchor: nil,
38 | options: nil
39 | )
40 | } else {
41 | throw AuthError.configuration(
42 | "WebAuthn is not supported on this platform",
43 | "WebAuthn requires iOS 17.4+, macOS 13.5+, or visionOS 1.0+",
44 | nil
45 | )
46 | }
47 |
48 | log.verbose("Passkey created successfully")
49 | setBusy(false)
50 | authenticatorState.setCurrentStep(.passkeyCreated)
51 | } catch {
52 | log.error("Passkey creation failed: \(error)")
53 | setBusy(false)
54 | let authenticationError = self.error(for: error)
55 | setMessage(authenticationError)
56 | throw authenticationError
57 | }
58 | }
59 |
60 | /// Skips passkey creation and continues with the authentication flow
61 | ///
62 | /// Automatically sets the Authenticator's next step accordingly, as well as the
63 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
64 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
65 | public func skip() async throws {
66 | setBusy(true)
67 |
68 | do {
69 | log.verbose("Skipping passkey creation")
70 | // Use post-passkey logic (attribute verification and sign-in)
71 | let nextStep = try await nextStepAfterPasskeyFlow()
72 |
73 | setBusy(false)
74 | authenticatorState.setCurrentStep(nextStep)
75 | } catch {
76 | log.error("Skip passkey creation failed")
77 | setBusy(false)
78 | let authenticationError = self.error(for: error)
79 | setMessage(authenticationError)
80 | throw authenticationError
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ContinueSignInWithTOTPSetupState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Continue Sign In with TOTP Setup content views, representing the ``Authenticator`` is the ``AuthenticatorStep/continueSignInWithTOTPSetup`` step.
13 | public class ContinueSignInWithTOTPSetupState: AuthenticatorBaseState {
14 | /// The confirmation code provided by the user
15 | @Published public var confirmationCode: String = ""
16 |
17 | private let issuer: String?
18 | private let totpSetupDetails: TOTPSetupDetails
19 |
20 | init(authenticatorState: AuthenticatorStateProtocol,
21 | issuer: String?,
22 | totpSetupDetails: TOTPSetupDetails) {
23 | self.totpSetupDetails = totpSetupDetails
24 | self.issuer = issuer
25 | super.init(authenticatorState: authenticatorState,
26 | credentials: Credentials())
27 | }
28 |
29 | /// The `Amplify.TOTPSetupDetails.sharedSecret` associated with this state.
30 | public var sharedSecret: String {
31 | return totpSetupDetails.sharedSecret
32 | }
33 |
34 | /// The `Amplify.TOTPSetupDetails.getSetupURI` associated with this state.
35 | public var setupURI: String {
36 | var setupURIAccountName: String = ""
37 | if let issuer = extractIssuerForQRCodeGeneration() {
38 | setupURIAccountName = issuer + ":" + totpSetupDetails.username
39 | return "otpauth://totp/\(setupURIAccountName)?secret=\(sharedSecret)" + "&issuer=\(issuer)"
40 | } else {
41 | return "otpauth://totp/\(setupURIAccountName)?secret=\(sharedSecret)"
42 |
43 | }
44 | }
45 |
46 | private func extractIssuerForQRCodeGeneration() -> String? {
47 | if let issuer = issuer {
48 | return issuer
49 | }
50 | log.warn("`totpOptions` not provided as part of initialization. Falling back to extract application name from Bundle.")
51 |
52 | if let applicationName = Bundle.main.applicationName {
53 | return applicationName
54 | }
55 | log.error("Unable to extract the application name from Bundle")
56 | return nil
57 | }
58 |
59 | /// Attempts to continue the user's sign in using the provided confirmation code.
60 | ///
61 | /// Automatically sets the Authenticator's next step accordingly, as well as the
62 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
63 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
64 | public func continueSignIn() async throws {
65 | setBusy(true)
66 |
67 | do {
68 | log.verbose("Attempting to confirm Sign In with Code")
69 | let result = try await authenticationService.confirmSignIn(
70 | challengeResponse: confirmationCode,
71 | options: nil
72 | )
73 | let nextStep = try await nextStep(for: result)
74 |
75 | setBusy(false)
76 |
77 | authenticatorState.setCurrentStep(nextStep)
78 | } catch {
79 | log.error("Confirm Sign In with Code failed")
80 | setBusy(false)
81 | let authenticationError = self.error(for: error)
82 | setMessage(authenticationError)
83 | throw authenticationError
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ContinueSignInWithEmailMFASetupStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ContinueSignInWithEmailMFASetupStateTests: XCTestCase {
13 | private var state: ContinueSignInWithEmailMFASetupState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | authenticatorState = MockAuthenticatorState()
19 | state = ContinueSignInWithEmailMFASetupState(credentials: Credentials())
20 | state.email = "test@test.com"
21 |
22 | authenticationService = MockAuthenticationService()
23 | authenticatorState.authenticationService = authenticationService
24 | state.configure(with: authenticatorState)
25 | }
26 |
27 | override func tearDown() {
28 | state = nil
29 | authenticatorState = nil
30 | authenticationService = nil
31 | }
32 |
33 | func testContinueSignIn_withSuccess_shouldSetNextStep() async throws {
34 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .confirmSignInWithOTP(.init(destination: .email("test@test.com"))))
35 | authenticationService.mockedCurrentUser = MockAuthenticationService.User(
36 | username: "username",
37 | userId: "userId"
38 | )
39 |
40 | try await state.continueSignIn()
41 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
42 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
43 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
44 | guard case .confirmSignInWithOTP = currentStep else {
45 | XCTFail("Expected confirmSignInWithOTP, was \(currentStep)")
46 | return
47 | }
48 | }
49 |
50 | func testContinueSignIn_withError_shouldSetErrorMessage() async throws {
51 | do {
52 | try await state.continueSignIn()
53 | XCTFail("Should not succeed")
54 | } catch {
55 | guard let authenticatorError = error as? AuthenticatorError else {
56 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
57 | return
58 | }
59 |
60 | let task = Task { @MainActor in
61 | XCTAssertNotNil(state.message)
62 | XCTAssertEqual(state.message?.content, authenticatorError.content)
63 | }
64 | await task.value
65 | }
66 | }
67 |
68 | func testContinueSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws {
69 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .done)
70 | do {
71 | try await state.continueSignIn()
72 | XCTFail("Should not succeed")
73 | } catch {
74 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
75 | guard let authenticatorError = error as? AuthenticatorError else {
76 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
77 | return
78 | }
79 |
80 | let task = Task { @MainActor in
81 | XCTAssertNotNil(state.message)
82 | XCTAssertEqual(state.message?.content, authenticatorError.content)
83 | }
84 | await task.value
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/SignInState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import SwiftUI
11 |
12 | /// The state observed by the Sign In content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/signIn`` step.
13 | public class SignInState: AuthenticatorBaseState {
14 | /// The username provided by the user. Note that this could be an email and a phone number as well.
15 | @Published public var username: String = "" {
16 | didSet {
17 | credentials.username = username
18 | }
19 | }
20 | /// The password provided by the user
21 | @Published public var password: String = "" {
22 | didSet {
23 | credentials.password = password
24 | }
25 | }
26 |
27 | /// Attempts to sign in using the provided credentials
28 | ///
29 | /// Automatically sets the Authenticator's next step accordingly, as well as the
30 | /// ``AuthenticatorBaseState/isBusy`` and `AuthenticatorBaseState/message` properties.
31 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
32 | public func signIn() async throws {
33 | setBusy(true)
34 |
35 | // Reset selected auth factor tracking for new sign-in flow
36 | credentials.selectedAuthFactor = nil
37 |
38 | do {
39 | log.verbose("Attempting to Sign In")
40 |
41 | // Translate AuthenticationFlow to Amplify AuthFlowType
42 | let signInOptions = createSignInOptions()
43 |
44 | let result = try await authenticationService.signIn(
45 | username: username.isEmpty ? nil : username,
46 | password: password.isEmpty ? nil : password,
47 | options: signInOptions
48 | )
49 | let nextStep = try await nextStep(for: result)
50 | setBusy(false)
51 | authenticatorState.setCurrentStep(nextStep)
52 | } catch {
53 | log.error("Unable to Sign In")
54 | setBusy(false)
55 | let authenticationError = self.error(for: error)
56 | setMessage(authenticationError)
57 | throw authenticationError
58 | }
59 | }
60 |
61 | /// Creates sign-in options based on the authentication flow configuration
62 | private func createSignInOptions() -> AuthSignInRequest.Options? {
63 | switch authenticationFlow {
64 | case .password:
65 | // Use standard SRP flow for password-only authentication
66 | return .init(pluginOptions: AWSAuthSignInOptions(authFlowType: .userSRP))
67 |
68 | case .userChoice(let preferredAuthFactor, _):
69 | // Use the AuthFactor extension to translate to AuthFactorType
70 | let preferredFirstFactor = preferredAuthFactor?.toAuthFactorType()
71 |
72 | // Use userAuth flow for user choice authentication
73 | return .init(pluginOptions: AWSAuthSignInOptions(
74 | authFlowType: .userAuth(preferredFirstFactor: preferredFirstFactor)
75 | ))
76 | }
77 | }
78 |
79 | /// Manually moves the Authenticator to a different initial step
80 | /// - Parameter initialStep: The desired ``AuthenticatorInitialStep``
81 | public func move(to initialStep: AuthenticatorInitialStep) {
82 | authenticatorState.move(to: initialStep)
83 | }
84 | }
85 |
86 | extension SignInState {
87 | enum Field: Int, Hashable, CaseIterable {
88 | case username
89 | case password
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/Authenticator/States/ConfirmVerifyUserState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// The state observed by the Confirm Verify User content view, representing the ``Authenticator`` is in the ``AuthenticatorStep/confirmVerifyUser`` step.
12 | public class ConfirmVerifyUserState: AuthenticatorBaseState {
13 | /// The confirmation code provided by the user
14 | @Published public var confirmationCode: String = ""
15 |
16 | /// The `Amplify.AuthUserAttributeKey` associated with this state. If the Authenticator is not in the `.confirmVerifyUser` step, it returns `nil`
17 | public var userAttributeKey: AuthUserAttributeKey? {
18 | guard case .confirmVerifyUser(let attribute, _) = authenticatorState.step else {
19 | return nil
20 | }
21 |
22 | return attribute
23 | }
24 |
25 | /// The `Amplify.AuthCodeDeliveryDetails` associated with this state. If the Authenticator is not in the `.confirmVerifyUser` step, it returns `nil`
26 | public var deliveryDetails: AuthCodeDeliveryDetails? {
27 | guard case .confirmVerifyUser(_, let deliveryDetails) = authenticatorState.step else {
28 | return nil
29 | }
30 |
31 | return deliveryDetails
32 | }
33 |
34 | /// Attempts to verify the associated ``userAttributeKey`` using the provided confirmation code
35 | ///
36 | /// Automatically sets the Authenticator's next step accordingly, as well as the
37 | /// `Amplify.AuthenticatorBaseState/isBusy` and ``AuthenticatorBaseState/message`` properties.
38 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
39 | public func confirmVerifyUser() async throws {
40 | guard let userAttributeKey = userAttributeKey else {
41 | return
42 | }
43 | setBusy(true)
44 |
45 | do {
46 | log.verbose("Attempting to confirm attribute \(userAttributeKey)")
47 | try await authenticationService.confirm(
48 | userAttribute: userAttributeKey,
49 | confirmationCode: confirmationCode,
50 | options: nil
51 | )
52 |
53 | let user = try await authenticationService.getCurrentUser()
54 |
55 | setBusy(false)
56 |
57 | authenticatorState.setCurrentStep(.signedIn(user: user))
58 | } catch {
59 | log.error("Unable to confirm user attribute")
60 | setBusy(false)
61 | let authenticationError = self.error(for: error)
62 | setMessage(authenticationError)
63 | throw authenticationError
64 | }
65 | }
66 |
67 | /// Skips the verification of the assocaited ``userAttributeKey`` and attempts to proceed with sign in
68 | ///
69 | /// Automatically sets the Authenticator's next step accordingly, as well as the
70 | /// ``AuthenticatorBaseState/isBusy`` and ``AuthenticatorBaseState/message`` properties.
71 | /// - Throws: An `Amplify.AuthenticationError` if the operation fails
72 | public func skip() async throws {
73 | setBusy(true)
74 |
75 | do {
76 | log.verbose("Skipping user verification")
77 | let user = try await authenticatorState.authenticationService.getCurrentUser()
78 | authenticatorState.setCurrentStep(.signedIn(user: user))
79 | setBusy(false)
80 | } catch {
81 | log.error("Unable to get the current user after skipping verification")
82 | log.error(error)
83 | setBusy(false)
84 | // Go back to sign in
85 | authenticatorState.setCurrentStep(.signIn)
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Models/AuthFactor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import AWSCognitoAuthPlugin
10 | import Foundation
11 |
12 | /// Represents an authentication factor that can be used during sign-in
13 | public enum AuthFactor: Equatable, Hashable {
14 | /// Password authentication with optional SRP (Secure Remote Password)
15 | case password(srp: Bool = true)
16 |
17 | /// Email-based one-time password authentication
18 | case emailOtp
19 |
20 | /// SMS-based one-time password authentication
21 | case smsOtp
22 |
23 | /// WebAuthn/Passkey authentication
24 | case webAuthn
25 | }
26 |
27 | extension AuthFactor: Codable {}
28 |
29 | extension AuthFactor {
30 | /// Translates AuthFactor to Amplify AuthFactorType
31 | func toAuthFactorType() -> AuthFactorType {
32 | switch self {
33 | case .password(let srp):
34 | return srp ? .passwordSRP : .password
35 | case .emailOtp:
36 | return .emailOTP
37 | case .smsOtp:
38 | return .smsOTP
39 | case .webAuthn:
40 | #if os(iOS) || os(macOS) || os(visionOS)
41 | if #available(iOS 17.4, macOS 13.5, visionOS 1.0, *) {
42 | return .webAuthn
43 | } else {
44 | // Fallback to password if WebAuthn not available
45 | return .passwordSRP
46 | }
47 | #else
48 | // Fallback to password on unsupported platforms
49 | return .passwordSRP
50 | #endif
51 | }
52 | }
53 |
54 | /// Returns true if this auth factor is a password-based factor (with or without SRP)
55 | var isPassword: Bool {
56 | if case .password = self {
57 | return true
58 | }
59 | return false
60 | }
61 |
62 | /// Returns true if this auth factor is password with SRP enabled
63 | var isPasswordWithSRP: Bool {
64 | guard case .password(let srp) = self else {
65 | return false
66 | }
67 | return srp
68 | }
69 |
70 | /// Display priority for sorting auth factors
71 | /// Lower values appear first: WebAuthn (1), SMS (2), Email (3), Password (4)
72 | var displayPriority: Int {
73 | switch self {
74 | case .webAuthn:
75 | return 1
76 | case .smsOtp:
77 | return 2
78 | case .emailOtp:
79 | return 3
80 | case .password:
81 | return 4
82 | }
83 | }
84 | }
85 |
86 | extension Array where Element == AuthFactor {
87 | /// Returns true if the array contains any password-based auth factor
88 | var containsPassword: Bool {
89 | return contains(where: { $0.isPassword })
90 | }
91 |
92 | /// Returns the preferred password-based auth factor
93 | /// Prefers passwordSRP over password when both are available (more secure)
94 | var preferredPasswordFactor: AuthFactor? {
95 | // First, try to find password with SRP (more secure)
96 | if let passwordSRP = first(where: { $0.isPasswordWithSRP }) {
97 | return passwordSRP
98 | }
99 |
100 | // Fall back to password without SRP
101 | return first(where: { $0.isPassword })
102 | }
103 |
104 | /// Returns all non-password auth factors sorted by priority
105 | /// Order: WebAuthn (Passkey), SMS OTP, Email OTP
106 | var nonPasswordFactors: [AuthFactor] {
107 | return filter { !$0.isPassword }.sorted { factor1, factor2 in
108 | return factor1.displayPriority < factor2.displayPriority
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ContinueSignInWithMFASelectionStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ContinueSignInWithMFASelectionStateTests: XCTestCase {
13 | private var state: ContinueSignInWithMFASelectionState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | authenticatorState = MockAuthenticatorState()
19 | state = ContinueSignInWithMFASelectionState(
20 | authenticatorState: authenticatorState,
21 | allowedMFATypes: [.sms, .totp])
22 | state.selectedMFAType = .totp
23 |
24 | authenticationService = MockAuthenticationService()
25 | authenticatorState.authenticationService = authenticationService
26 | state.configure(with: authenticatorState)
27 | }
28 |
29 | override func tearDown() {
30 | state = nil
31 | authenticatorState = nil
32 | authenticationService = nil
33 | }
34 |
35 | func testContinueSignIn_withSuccess_shouldSetNextStep() async throws {
36 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .confirmSignInWithTOTPCode)
37 | authenticationService.mockedCurrentUser = MockAuthenticationService.User(
38 | username: "username",
39 | userId: "userId"
40 | )
41 |
42 | try await state.continueSignIn()
43 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
44 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
45 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
46 | guard case .confirmSignInWithTOTPCode = currentStep else {
47 | XCTFail("Expected confirmSignInWithTOTPCode, was \(currentStep)")
48 | return
49 | }
50 | }
51 |
52 | func testContinueSignIn_withError_shouldSetErrorMessage() async throws {
53 | do {
54 | try await state.continueSignIn()
55 | XCTFail("Should not succeed")
56 | } catch {
57 | guard let authenticatorError = error as? AuthenticatorError else {
58 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
59 | return
60 | }
61 |
62 | let task = Task { @MainActor in
63 | XCTAssertNotNil(state.message)
64 | XCTAssertEqual(state.message?.content, authenticatorError.content)
65 | }
66 | await task.value
67 | }
68 | }
69 |
70 | func testContinueSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws {
71 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .done)
72 | do {
73 | try await state.continueSignIn()
74 | XCTFail("Should not succeed")
75 | } catch {
76 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
77 | guard let authenticatorError = error as? AuthenticatorError else {
78 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
79 | return
80 | }
81 |
82 | let task = Task { @MainActor in
83 | XCTAssertNotNil(state.message)
84 | XCTAssertEqual(state.message?.content, authenticatorError.content)
85 | }
86 | await task.value
87 | }
88 | }
89 |
90 | func testAllowedMFATypes_onContinueSignInWithMFACodeSelection_shouldReturnDetails() throws {
91 |
92 | authenticatorState.mockedStep = .continueSignInWithMFASelection(allowedMFATypes: [.sms, .totp])
93 |
94 | let allowedMFATypes = try XCTUnwrap(state.allowedMFATypes)
95 | XCTAssertEqual(allowedMFATypes, [.sms, .totp])
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ConfirmResetPasswordStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ConfirmResetPasswordStateTests: XCTestCase {
13 | private var state: ConfirmResetPasswordState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | state = ConfirmResetPasswordState(credentials: Credentials())
19 | state.credentials.username = "username"
20 | authenticatorState = MockAuthenticatorState()
21 | authenticationService = MockAuthenticationService()
22 | authenticatorState.authenticationService = authenticationService
23 | state.configure(with: authenticatorState)
24 | }
25 |
26 | override func tearDown() {
27 | state = nil
28 | authenticatorState = nil
29 | authenticationService = nil
30 | }
31 |
32 | func testConfirmResetPassword_withSuccess_andSignIn_shouldSetNextStep() async throws {
33 | authenticationService.mockedSignInResult = .init(nextStep: .done)
34 | authenticationService.mockedCurrentUser = MockAuthenticationService.User(
35 | username: "username",
36 | userId: "userId"
37 | )
38 |
39 | try await state.confirmResetPassword()
40 | XCTAssertEqual(authenticationService.confirmResetPasswordCount, 1)
41 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
42 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
43 | guard case .signedIn(_) = currentStep else {
44 | XCTFail("Expected signedIn, was \(currentStep)")
45 | return
46 | }
47 | }
48 |
49 | func testConfirmResetPassword_withError_shouldSetErrorMessage() async throws {
50 | authenticationService.mockedConfirmResetPasswordError = .error(message: "Unable to confirm reset password")
51 | do {
52 | try await state.confirmResetPassword()
53 | XCTFail("Should not succeed")
54 | } catch {
55 | guard let authenticatorError = error as? AuthenticatorError else {
56 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
57 | return
58 | }
59 |
60 | let task = Task { @MainActor in
61 | XCTAssertNotNil(state.message)
62 | XCTAssertEqual(state.message?.content, authenticatorError.content)
63 | }
64 | await task.value
65 | }
66 | }
67 |
68 | func testConfirmResetPassword_withSuccess_andFailedToSignIn_shouldSetNextStep() async throws {
69 | try await state.confirmResetPassword()
70 | XCTAssertEqual(authenticationService.confirmResetPasswordCount, 1)
71 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
72 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
73 | guard case .signIn = currentStep else {
74 | XCTFail("Expected signIn, was \(currentStep)")
75 | return
76 | }
77 | }
78 |
79 | func testDeliveryDetails_onConfirmResetPasswordStep_shouldReturnDetails() throws {
80 | let destination = DeliveryDestination.sms("123456789")
81 | authenticatorState.mockedStep = .confirmResetPassword(deliveryDetails: .init(destination: destination))
82 |
83 | let deliveryDetails = try XCTUnwrap(state.deliveryDetails)
84 | XCTAssertEqual(deliveryDetails.destination, destination)
85 | }
86 |
87 | func testDeliveryDetails_onUnexpectedStep_shouldRetunNil() throws {
88 | let destination = DeliveryDestination.sms("123456789")
89 | authenticatorState.mockedStep = .confirmSignUp(deliveryDetails: .init(destination: destination))
90 |
91 | XCTAssertNil(state.deliveryDetails)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorTests/States/ContinueSignInWithMFASetupSelectionStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | @testable import Authenticator
10 | import XCTest
11 |
12 | class ContinueSignInWithMFASetupSelectionStateTests: XCTestCase {
13 | private var state: ContinueSignInWithMFASetupSelectionState!
14 | private var authenticatorState: MockAuthenticatorState!
15 | private var authenticationService: MockAuthenticationService!
16 |
17 | override func setUp() {
18 | authenticatorState = MockAuthenticatorState()
19 | state = ContinueSignInWithMFASetupSelectionState(
20 | authenticatorState: authenticatorState,
21 | allowedMFATypes: [.sms, .totp, .email])
22 | state.selectedMFATypeToSetup = MFAType.email
23 |
24 | authenticationService = MockAuthenticationService()
25 | authenticatorState.authenticationService = authenticationService
26 | state.configure(with: authenticatorState)
27 | }
28 |
29 | override func tearDown() {
30 | state = nil
31 | authenticatorState = nil
32 | authenticationService = nil
33 | }
34 |
35 | func testContinueSignIn_withSuccess_shouldSetNextStep() async throws {
36 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .continueSignInWithMFASetupSelection([.sms, .totp, .email]))
37 | authenticationService.mockedCurrentUser = MockAuthenticationService.User(
38 | username: "username",
39 | userId: "userId"
40 | )
41 |
42 | try await state.continueSignIn()
43 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
44 | XCTAssertEqual(authenticatorState.setCurrentStepCount, 1)
45 | let currentStep = try XCTUnwrap(authenticatorState.setCurrentStepValue)
46 | guard case .continueSignInWithMFASetupSelection = currentStep else {
47 | XCTFail("Expected continueSignInWithMFASetupSelection, was \(currentStep)")
48 | return
49 | }
50 | }
51 |
52 | func testContinueSignIn_withError_shouldSetErrorMessage() async throws {
53 | do {
54 | try await state.continueSignIn()
55 | XCTFail("Should not succeed")
56 | } catch {
57 | guard let authenticatorError = error as? AuthenticatorError else {
58 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
59 | return
60 | }
61 |
62 | let task = Task { @MainActor in
63 | XCTAssertNotNil(state.message)
64 | XCTAssertEqual(state.message?.content, authenticatorError.content)
65 | }
66 | await task.value
67 | }
68 | }
69 |
70 | func testContinueSignIn_withSuccess_andFailedToSignIn_shouldSetErrorMessage() async throws {
71 | authenticationService.mockedConfirmSignInResult = .init(nextStep: .done)
72 | do {
73 | try await state.continueSignIn()
74 | XCTFail("Should not succeed")
75 | } catch {
76 | XCTAssertEqual(authenticationService.confirmSignInCount, 1)
77 | guard let authenticatorError = error as? AuthenticatorError else {
78 | XCTFail("Expected AuthenticatorError, was \(type(of: error))")
79 | return
80 | }
81 |
82 | let task = Task { @MainActor in
83 | XCTAssertNotNil(state.message)
84 | XCTAssertEqual(state.message?.content, authenticatorError.content)
85 | }
86 | await task.value
87 | }
88 | }
89 |
90 | func testAllowedMFATypes_onContinueSignInWithMFACodeSelection_shouldReturnDetails() throws {
91 |
92 | authenticatorState.mockedStep = .continueSignInWithMFASelection(allowedMFATypes: [.sms, .totp, .email])
93 |
94 | let allowedMFATypes = try XCTUnwrap(state.allowedMFATypes)
95 | XCTAssertEqual(allowedMFATypes, [.sms, .totp, .email])
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/AuthenticatorHostApp/AuthenticatorHostApp.xcodeproj/xcshareddata/xcschemes/AuthenticatorHostApp.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
34 |
35 |
36 |
37 |
40 |
46 |
47 |
48 |
49 |
50 |
60 |
62 |
68 |
69 |
70 |
71 |
77 |
79 |
85 |
86 |
87 |
88 |
90 |
91 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Utilities/AuthenticatorField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AuthenticatorField: View {
11 | @Environment(\.isEnabled) private var isEnabled: Bool
12 | private var isFocused: Bool
13 | @Environment(\.authenticatorOptions) private var options
14 | @Environment(\.authenticatorTheme) var theme
15 | @ObservedObject private var validator: Validator
16 | private let label: String?
17 | private let placeholder: String
18 | private let content: Content
19 |
20 | init(_ label: String?,
21 | placeholder: String,
22 | validator: Validator,
23 | isFocused: Bool,
24 | content: () -> Content) {
25 | self.label = label
26 | self.placeholder = placeholder
27 | self.validator = validator
28 | self.isFocused = isFocused
29 | self.content = content()
30 | }
31 |
32 | var body: some View {
33 | VStack(alignment: .leading, spacing: theme.components.field.spacing.vertical) {
34 | if let label = label {
35 | SwiftUI.Text(label)
36 | .foregroundColor(foregroundColor)
37 | .font(theme.fonts.body)
38 | .accessibilityHidden(true)
39 | }
40 |
41 | content
42 | .background(
43 | RoundedRectangle(cornerRadius: theme.components.field.cornerRadius, style: .continuous)
44 | .fill(backgroundColor)
45 | )
46 | .overlay(
47 | RoundedRectangle(cornerRadius: theme.components.field.cornerRadius)
48 | .stroke(borderColor,
49 | lineWidth: borderWidth)
50 |
51 | )
52 |
53 | if let errorMessage = errorMessage {
54 | SwiftUI.Text(errorMessage)
55 | .font(theme.fonts.subheadline)
56 | .foregroundColor(foregroundColor)
57 | .transition(options.contentTransition)
58 | .accessibilityHidden(true)
59 | }
60 |
61 | }
62 | .accessibilityElement(children: .contain)
63 | .accessibilityLabel(accessibilityLabel)
64 | .animation(options.contentAnimation, value: validator.state)
65 | }
66 |
67 | private var backgroundColor: Color {
68 | isEnabled ? theme.components.field.backgroundColor : Color(
69 | light: theme.colors.background.disabled,
70 | dark: .clear
71 | )
72 | }
73 |
74 | private var foregroundColor: Color {
75 | switch validator.state {
76 | case .normal:
77 | return theme.colors.foreground.secondary
78 | case .error:
79 | return theme.colors.foreground.error
80 | }
81 | }
82 |
83 | private var borderColor: Color {
84 | switch validator.state {
85 | case .normal:
86 | return isFocused ?
87 | theme.colors.border.interactive : theme.colors.border.primary
88 |
89 | case .error:
90 | return theme.colors.border.error
91 | }
92 | }
93 |
94 | private var borderWidth: CGFloat {
95 | let width = theme.components.field.borderWidth
96 | return isFocused ? width + 1 : width
97 | }
98 |
99 | private var title: String {
100 | return label ?? placeholder
101 | }
102 |
103 | private var errorMessage: String? {
104 | if case .error(let message) = validator.state,
105 | let message = message {
106 | return String(format: message, title)
107 | }
108 | return nil
109 | }
110 |
111 | private var accessibilityLabel: Text {
112 | if let errorMessage = errorMessage {
113 | return Text("\(errorMessage). \(title)")
114 | }
115 |
116 | return Text(title)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/VerifyUserView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/verifyUser`` step.
12 | public struct VerifyUserView: View {
14 | @Environment(\.authenticatorState) private var authenticatorState
15 | @ObservedObject private var state: VerifyUserState
16 | private let headerContent: Header
17 | private let footerContent: Footer
18 |
19 | /// Creates a `VerifyUserView`
20 | /// - Parameter state: The ``VerifyUserState`` that is observed by this view
21 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``VerifyUserHeader``
22 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to `SwiftUI.EmptyView`
23 | public init(
24 | state: VerifyUserState,
25 | @ViewBuilder headerContent: () -> Header = {
26 | VerifyUserHeader()
27 | },
28 | @ViewBuilder footerContent: () -> Footer = {
29 | EmptyView()
30 | }
31 | ) {
32 | self._state = ObservedObject(wrappedValue: state)
33 | self.headerContent = headerContent()
34 | self.footerContent = footerContent()
35 | }
36 |
37 | public var body: some View {
38 | AuthenticatorView(isBusy: state.isBusy) {
39 | headerContent
40 |
41 | ForEach(state.unverifiedFields, id: \.self) { field in
42 | RadioButton(
43 | label: field.localizedTitle,
44 | isSelected: .constant(state.selectedField == field)
45 | ) {
46 | state.selectedField = field
47 | }
48 | .accessibilityAddTraits(state.selectedField == field ? .isSelected : .isButton)
49 | .animation(.none, value: state.selectedField)
50 | }
51 |
52 | Button("authenticator.verifyUser.button.verify".localized()) {
53 | Task {
54 | await verifyUser()
55 | }
56 | }
57 | .buttonStyle(.primary)
58 | .disabled(state.selectedField == nil)
59 | .opacity(state.selectedField == nil ? 0.5 : 1)
60 |
61 | Button("authenticator.verifyUser.button.skip".localized()) {
62 | Task {
63 | await skip()
64 | }
65 | }
66 | .buttonStyle(.link)
67 |
68 | footerContent
69 | }
70 | .messageBanner($state.message)
71 | .task {
72 | // If we somehow ended up in this view with no attributes to verify, automatically skip
73 | if state.unverifiedFields.isEmpty {
74 | await skip()
75 | }
76 | }
77 | }
78 |
79 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
80 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
81 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
82 | state.errorTransform = errorTransform
83 | return self
84 | }
85 |
86 | private func verifyUser() async {
87 | try? await state.verifyUser()
88 | }
89 |
90 | private func skip() async {
91 | try? await state.skip()
92 | }
93 | }
94 |
95 | /// Default header for the ``VerifyUserView``. It displays the view's title
96 | public struct VerifyUserHeader: View {
97 | @Environment(\.authenticatorTheme) private var theme
98 |
99 | public init() {}
100 | public var body: some View {
101 | DefaultHeader(
102 | title: "authenticator.verifyUser.title".localized()
103 | )
104 | .font(theme.fonts.title3)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/ConfirmSignInWithCodeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | struct ConfirmSignInWithCodeView: View {
13 | @Environment(\.authenticatorState) private var authenticatorState
14 | @StateObject private var codeValidator: Validator
15 | @ObservedObject private var state: ConfirmSignInWithCodeState
16 | private let headerContent: Header
17 | private let footerContent: Footer
18 |
19 | init(
20 | state: ConfirmSignInWithCodeState,
21 | @ViewBuilder headerContent: () -> Header = {
22 | EmptyView()
23 | },
24 | @ViewBuilder footerContent: () -> Footer = {
25 | EmptyView()
26 | },
27 | errorTransform: ((AuthError) -> AuthenticatorError?)? = nil
28 | ) {
29 | self.state = state
30 | self.headerContent = headerContent()
31 | self.footerContent = footerContent()
32 | self._codeValidator = StateObject(wrappedValue: Validator(
33 | using: FieldValidators.required
34 | ))
35 | }
36 |
37 | private var currentMFAType: AuthenticatorFactorType {
38 | switch authenticatorState.step {
39 | case .confirmSignInWithMFACode(let deliveryDetails),
40 | .confirmSignInWithOTP(let deliveryDetails):
41 | switch deliveryDetails?.destination {
42 | case .email:
43 | return .email
44 | case .phone:
45 | return .sms
46 | default:
47 | return .none
48 | }
49 | case .confirmSignInWithTOTPCode:
50 | return .totp
51 | default:
52 | return .none
53 | }
54 | }
55 |
56 | private var textFieldLabel: String {
57 | switch currentMFAType {
58 | case .sms, .none, .email:
59 | return "authenticator.field.code.label".localized()
60 | case .totp:
61 | return "authenticator.field.totp.code.label".localized()
62 | }
63 | }
64 |
65 | private var textFieldPlaceholder: String {
66 | switch currentMFAType {
67 | case .sms, .none, .email:
68 | return "authenticator.field.code.placeholder".localized()
69 | case .totp:
70 | return "authenticator.field.totp.code.placeholder".localized()
71 | }
72 | }
73 |
74 | private var submitButtonTitle: String {
75 | switch currentMFAType {
76 | case .sms, .none, .email:
77 | return "authenticator.confirmSignInWithCode.button.submit".localized()
78 | case .totp:
79 | return "authenticator.confirmSignInWithCode.totp.button.submit".localized()
80 | }
81 | }
82 |
83 | var body: some View {
84 | AuthenticatorView(isBusy: state.isBusy) {
85 | headerContent
86 |
87 | TextField(
88 | textFieldLabel,
89 | text: $state.confirmationCode,
90 | placeholder: textFieldPlaceholder,
91 | validator: codeValidator
92 | )
93 | .textContentType(.oneTimeCode)
94 | #if os(iOS)
95 | .keyboardType(.default)
96 | #endif
97 |
98 | Button(submitButtonTitle) {
99 | Task { await confirmSignIn() }
100 | }
101 | .buttonStyle(.primary)
102 |
103 | footerContent
104 | }
105 | .messageBanner($state.message)
106 | .onSubmit {
107 | Task {
108 | await confirmSignIn()
109 | }
110 | }
111 | }
112 |
113 | private func confirmSignIn() async {
114 | guard codeValidator.validate() else {
115 | log.verbose("Code validation failed")
116 | return
117 | }
118 |
119 | try? await state.confirmSignIn()
120 | }
121 | }
122 |
123 | extension ConfirmSignInWithCodeView: AuthenticatorLogging {}
124 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/SignUpInputField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SignUpInputField: View {
11 | @Environment(\.authenticatorOptions) private var options
12 | @Environment(\.authenticatorTheme) var theme
13 | @ObservedObject private var field: SignUpState.Field
14 | @ObservedObject private var validator: Validator
15 |
16 | init(
17 | field: SignUpState.Field,
18 | validator: Validator
19 | ) {
20 | self.field = field
21 | self.validator = validator
22 | }
23 |
24 | var body: some View {
25 | Group {
26 | if let customField = field.field as? CustomSignUpField {
27 | customView(for: customField)
28 | } else if let baseField = field.field as? BaseSignUpField {
29 | regularView(for: baseField)
30 | }
31 | }
32 | }
33 |
34 | @ViewBuilder func regularView(for field: BaseSignUpField) -> some View {
35 | Group {
36 | switch field.inputType {
37 | case .text:
38 | TextField(
39 | field.displayedLabel,
40 | text: $field.value,
41 | placeholder: field.placeholder,
42 | validator: validator
43 | )
44 | case .password:
45 | PasswordField(
46 | field.displayedLabel,
47 | text: $field.value,
48 | placeholder: field.placeholder,
49 | validator: validator
50 | )
51 | case .date:
52 | DatePicker(
53 | field.displayedLabel,
54 | text: $field.value,
55 | placeholder: field.placeholder,
56 | validator: validator
57 | )
58 | case .phoneNumber:
59 | PhoneNumberField(
60 | field.displayedLabel,
61 | text: $field.value,
62 | placeholder: field.placeholder,
63 | validator: validator
64 | )
65 | }
66 | }
67 | .textContentType(field.attributeType.textContentType)
68 | #if os(iOS)
69 | .keyboardType(field.attributeType.keyboardType)
70 | #endif
71 | }
72 |
73 | @ViewBuilder func customView(for field: CustomSignUpField) -> some View {
74 | VStack(alignment: .leading, spacing: theme.components.field.spacing.vertical) {
75 | if let label = field.displayedLabel {
76 | HStack {
77 | SwiftUI.Text(label)
78 | .foregroundColor(foregroundColor)
79 | .font(theme.fonts.body)
80 | .accessibilityHidden(true)
81 | Spacer()
82 | }
83 | }
84 | AnyView(
85 | field.content($field.value)
86 | )
87 | .onChange(of: self.field.value) { _ in
88 | validator.validate()
89 | }
90 | .onAppear {
91 | validator.value = $field.value
92 | }
93 | if case .error(let message) = validator.state, let errorMessage = message {
94 | AnyView(
95 | field.errorContent(String(format: errorMessage, field.label ?? "authenticator.validator.field".localized()))
96 | .font(theme.fonts.subheadline)
97 | )
98 | .foregroundColor(foregroundColor)
99 | .transition(options.contentTransition)
100 | .accessibilityHidden(true)
101 | }
102 | }
103 | }
104 |
105 | private var foregroundColor: Color {
106 | switch validator.state {
107 | case .normal:
108 | return theme.colors.foreground.secondary
109 | case .error:
110 | return theme.colors.foreground.error
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/PromptToCreatePasskeyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import Amplify
9 | import SwiftUI
10 |
11 | /// Represents the content being displayed when the ``Authenticator`` is in the ``AuthenticatorStep/promptToCreatePasskey`` step.
12 | public struct PromptToCreatePasskeyView: View {
14 | @Environment(\.authenticatorState) private var authenticatorState
15 | @Environment(\.authenticatorTheme) var theme
16 | @ObservedObject private var state: PromptToCreatePasskeyState
17 | private let headerContent: Header
18 | private let footerContent: Footer
19 |
20 | /// Creates a `PromptToCreatePasskeyView`
21 | /// - Parameter state: The ``PromptToCreatePasskeyState`` that is observed by this view
22 | /// - Parameter headerContent: The content displayed above the fields. Defaults to ``PromptToCreatePasskeyHeader``
23 | /// - Parameter footerContent: The content displayed bellow the fields. Defaults to ``PromptToCreatePasskeyFooter``
24 | public init(
25 | state: PromptToCreatePasskeyState,
26 | @ViewBuilder headerContent: () -> Header = {
27 | PromptToCreatePasskeyHeader()
28 | },
29 | @ViewBuilder footerContent: () -> Footer = {
30 | PromptToCreatePasskeyFooter()
31 | }
32 | ) {
33 | self._state = ObservedObject(wrappedValue: state)
34 | self.headerContent = headerContent()
35 | self.footerContent = footerContent()
36 | }
37 |
38 | public var body: some View {
39 | AuthenticatorView(isBusy: state.isBusy) {
40 | headerContent
41 |
42 | Text("authenticator.promptToCreatePasskey.description".localized())
43 | .font(theme.fonts.body)
44 | .foregroundColor(theme.colors.foreground.primary)
45 | .multilineTextAlignment(.leading)
46 | .padding(.bottom, 16)
47 |
48 | // Passkey illustration
49 | Image("passkey", bundle: .module)
50 | .resizable()
51 | .scaledToFit()
52 | .frame(height: 120)
53 | .padding(.vertical, 24)
54 |
55 | Button("authenticator.promptToCreatePasskey.button.createPasskey".localized()) {
56 | Task {
57 | await createPasskey()
58 | }
59 | }
60 | .buttonStyle(.primary)
61 |
62 | Button("authenticator.promptToCreatePasskey.button.skip".localized()) {
63 | Task {
64 | await skip()
65 | }
66 | }
67 | .buttonStyle(.link)
68 |
69 | footerContent
70 | }
71 | .messageBanner($state.message)
72 | }
73 |
74 | /// Sets a custom error mapping function for the `AuthError`s that are displayed
75 | /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed.
76 | public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError?) -> Self {
77 | state.errorTransform = errorTransform
78 | return self
79 | }
80 |
81 | private func createPasskey() async {
82 | try? await state.createPasskey()
83 | }
84 |
85 | private func skip() async {
86 | try? await state.skip()
87 | }
88 | }
89 |
90 | extension PromptToCreatePasskeyView: AuthenticatorLogging {}
91 |
92 | /// Default header for the ``PromptToCreatePasskeyView``. It displays the view's title
93 | public struct PromptToCreatePasskeyHeader: View {
94 | public init() {}
95 | public var body: some View {
96 | DefaultHeader(
97 | title: "authenticator.promptToCreatePasskey.title".localized()
98 | )
99 | }
100 | }
101 |
102 | /// Default footer for the ``PromptToCreatePasskeyView``.
103 | public struct PromptToCreatePasskeyFooter: View {
104 | public init() {}
105 | public var body: some View {
106 | EmptyView()
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Primitives/TextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// This field allows the user to enter any text-based input
11 | /// It applies Amplify UI theming
12 | struct TextField: View {
13 | @Environment(\.authenticatorTheme) var theme
14 | @ObservedObject private var validator: Validator
15 | @Binding private var text: String
16 | @FocusState private var isFocused: Bool
17 | private let label: String?
18 | private let placeholder: String
19 | private var image: Image?
20 |
21 | init(_ label: String,
22 | text: Binding,
23 | placeholder: String,
24 | validator: Validator? = nil) {
25 | self.label = label
26 | self._text = text
27 | self.placeholder = placeholder
28 | self.validator = validator ?? .init(
29 | using: FieldValidators.none
30 | )
31 | self.validator.value = text
32 | }
33 |
34 | init(_ placeholder: String,
35 | text: Binding,
36 | validator: Validator? = nil) {
37 | self.label = nil
38 | self._text = text
39 | self.placeholder = placeholder
40 | self.validator = validator ?? .init(
41 | using: FieldValidators.none
42 | )
43 | self.validator.value = text
44 | }
45 |
46 | var body: some View {
47 | AuthenticatorField(
48 | label,
49 | placeholder: placeholder,
50 | validator: validator,
51 | isFocused: isFocused
52 | ) {
53 | HStack {
54 | SwiftUI.TextField("", text: $text, prompt: createPlaceHolderView(label: placeholder))
55 | .disableAutocorrection(true)
56 | .focused($isFocused)
57 | .onChange(of: text) { text in
58 | if validator.state != .normal || !text.isEmpty {
59 | validator.validate()
60 | }
61 | }
62 | .onChange(of: isFocused) { isFocused in
63 | if !isFocused {
64 | validator.validate()
65 | }
66 | }
67 | .textFieldStyle(.plain)
68 | .foregroundColor(foregroundColor)
69 | .frame(height: Platform.isMacOS ? 20 : 25)
70 | .padding([.top, .bottom, .leading], theme.components.field.padding)
71 | #if os(iOS)
72 | .autocapitalization(.none)
73 | #endif
74 |
75 | if shouldDisplayClearButton {
76 | ImageButton(.clear) {
77 | text = ""
78 | }
79 | .tintColor(clearButtonColor)
80 | .padding([.top, .bottom, .trailing], theme.components.field.padding)
81 | }
82 | }
83 | }
84 | }
85 |
86 | private func createPlaceHolderView(label: String) -> SwiftUI.Text {
87 | let textView = SwiftUI.Text(label)
88 | .foregroundColor(theme.colors.foreground.disabled.opacity(0.6))
89 | .font(theme.fonts.body)
90 | _ = textView.accessibilityHidden(true)
91 | return textView
92 | }
93 |
94 | private var foregroundColor: Color {
95 | switch validator.state {
96 | case .normal:
97 | return theme.colors.foreground.secondary
98 | case .error:
99 | return theme.colors.foreground.error
100 | }
101 | }
102 |
103 | private var clearButtonColor: Color {
104 | switch validator.state {
105 | case .normal:
106 | return isFocused ?
107 | theme.colors.border.interactive : theme.colors.border.primary
108 | case .error:
109 | return theme.colors.border.error
110 | }
111 | }
112 |
113 | private var shouldDisplayClearButton: Bool {
114 | // Show the clear button when there's text and
115 | // the field is focused on non-macOS platforms
116 | return !text.isEmpty && (Platform.isMacOS || isFocused)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/Sources/Authenticator/Views/Internal/AuthenticatorMessageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright Amazon.com Inc. or its affiliates.
3 | // All Rights Reserved.
4 | //
5 | // SPDX-License-Identifier: Apache-2.0
6 | //
7 |
8 | import SwiftUI
9 |
10 | extension View {
11 | func messageBanner(_ message: Binding) -> some View {
12 | self.modifier(AuthenticatorMessageModifier(message: message))
13 | }
14 | }
15 |
16 | private struct AuthenticatorMessageModifier: ViewModifier {
17 | @Environment(\.authenticatorOptions) var options
18 | @Binding var message: AuthenticatorMessage?
19 |
20 | @State private var dismissTimer: Timer? {
21 | willSet {
22 | dismissTimer?.invalidate()
23 | }
24 | }
25 |
26 | func body(content: Content) -> some View {
27 | #if os(iOS)
28 | content
29 | .fullScreenCover(isPresented: .constant(message != nil)) {
30 | messageContent
31 | }
32 | #elseif os(macOS)
33 | ZStack {
34 | content
35 | messageContent
36 | }
37 | #endif
38 | }
39 |
40 | private func dismissErrorView() {
41 | dismissTimer = nil
42 | message = nil
43 | }
44 |
45 | @ViewBuilder private var messageContent: some View {
46 | VStack {
47 | if let message = message {
48 | Spacer()
49 | AuthenticatorMessageView(
50 | message: message,
51 | action: dismissErrorView
52 | )
53 | .onAppear {
54 | dismissTimer = Timer.scheduledTimer(
55 | withTimeInterval: 5,
56 | repeats: false
57 | ) { _ in
58 | dismissErrorView()
59 | }
60 | }
61 | #if os(macOS)
62 | .transition(.move(edge: .bottom).combined(with: .opacity))
63 | #endif
64 | }
65 | }
66 | #if os(iOS)
67 | .background(ClearBackgroundView()
68 | .onTapGesture {
69 | dismissErrorView()
70 | }
71 | )
72 | #elseif os(macOS)
73 | .animation(options.contentAnimation, value: self.message != nil)
74 | #endif
75 | }
76 | }
77 |
78 | private struct AuthenticatorMessageView: View {
79 | @Environment(\.authenticatorTheme) private var theme
80 | let message: AuthenticatorMessage
81 | let action: () -> ()
82 |
83 | var body: some View {
84 | HStack {
85 | Text(message.content)
86 | .font(theme.fonts.callout)
87 | Spacer()
88 | ImageButton(.close) {
89 | action()
90 | }
91 | .tintColor(foregroundColor)
92 | }
93 | .frame(maxWidth: .infinity)
94 | .foregroundColor(foregroundColor)
95 | .padding(theme.components.alert.padding/2)
96 | .background(backgroundColor)
97 | .cornerRadius(theme.components.alert.cornerRadius)
98 | .padding(theme.components.alert.padding/2)
99 | }
100 |
101 | private var foregroundColor: Color {
102 | switch message.style {
103 | case .error:
104 | return theme.colors.foreground.error
105 | case .info:
106 | return theme.colors.foreground.info
107 | default:
108 | return theme.colors.foreground.primary
109 | }
110 | }
111 |
112 | private var backgroundColor: Color {
113 | switch message.style {
114 | case .error:
115 | return theme.colors.background.error
116 | case .info:
117 | return theme.colors.background.info
118 | default:
119 | return theme.colors.background.primary
120 | }
121 | }
122 | }
123 |
124 | #if os(iOS)
125 | struct ClearBackgroundView: UIViewRepresentable {
126 | func makeUIView(context: Context) -> UIView {
127 | return InnerView()
128 | }
129 |
130 | func updateUIView(_ uiView: UIView, context: Context) { }
131 |
132 | private class InnerView: UIView {
133 | override func didMoveToWindow() {
134 | super.didMoveToWindow()
135 | superview?.superview?.backgroundColor = .clear
136 | }
137 | }
138 | }
139 | #endif
140 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.3.0 (2025-12-15)
4 | - Add support for passwordless auth flows
5 |
6 | ## 1.2.3 (2024-12-16)
7 |
8 | ### Bug Fixes
9 | - **Theming**: Adding support for customizing the TextFields' input and placeholder foreground colours (#100)
10 | - **Authenticator**: Avoiding signing out users when the token refresh fails due to no connectivity (#104)
11 |
12 | ## 1.2.2 (2024-11-26)
13 |
14 | ### Misc. Updates
15 | - Updating code to support Amplify 2.45+
16 |
17 | ## 1.2.1 (2024-11-21)
18 |
19 | ### Misc. Updates
20 | - Pinning the Amplify version up to 2.44.x
21 |
22 | ## 1.2.0 (2024-10-31)
23 |
24 | ### Feature
25 | - **Authenticator**: Adding support for Email MFA (#96)
26 |
27 | ## 1.1.8 (2024-09-20)
28 |
29 | ### Bug Fixes
30 | - **Authenticator**: Adding new error localizations for limits exceeded (#96)
31 |
32 | ## 1.1.7 (2024-09-13)
33 |
34 | ### Bug Fixes
35 | - **Authenticator**: Allowing to only override the desired errors when invoking the errorMap functions (#93)
36 |
37 | ## 1.1.6 (2024-08-13)
38 |
39 | ### Bug Fixes
40 | - **Authenticator**: Properly handling expired sessions when loading the component (#87)
41 |
42 | ## 1.1.5 (2024-07-02)
43 |
44 | ### Bug Fixes
45 | - **Authenticator**: Setting corner radius according to the theme (#84)
46 |
47 | ## 1.1.4 (2024-06-07)
48 |
49 | ### Bug Fixes
50 | - **Authenticator**: Showing the proper message when there's connectivity issues (#82)
51 |
52 | ### Misc. Updates
53 | - Updating code to support Amplify 2.35+. (#82)
54 |
55 | ## 1.1.3 (2024-06-04)
56 |
57 | ### Bug Fixes
58 | - **SignUp**: Sign in fails when user is auto confirmed after sign up (#72)
59 |
60 | ### Misc. Updates
61 | - Pinning the Amplify version up to 2.34.x
62 |
63 | ## 1.1.2 (2024-04-26)
64 |
65 | ### Bug Fixes
66 |
67 | - **AuthenticatorState**: Making `move(to:)` public (#66)
68 | - **ConfirmSignUp**: Updating the state's `deliveryDetails` property when a new code is sent (#65)
69 |
70 | ## 1.1.1 (2024-03-11)
71 |
72 | ### Bug Fixes
73 | - Fixing phone numbers containing special characters being rejected by Cognito (See [#56](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/56))
74 |
75 | ### Misc. Updates
76 | - Using the new `sendVerificationCode` API (See [#54](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/54))
77 |
78 | ## 1.1.0 (2023-11-01)
79 |
80 | ### Features
81 | - Adding TOTP support (See [#31](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/43))
82 |
83 | ## 1.0.6 (2023-09-14)
84 |
85 | ### Misc. Updates
86 | - Updating code to support Amplify 2.16+. However, **TOTP** workflows are **not** yet supported.
87 |
88 | ## 1.0.5 (2023-08-31)
89 |
90 | ### Bug Fixes
91 | - Fixing required Sign Up attributes being displayed as optionals
92 | - Fixing Sign Up fields not being populated when providing a `signUpContent`
93 | - Fixing DatePicker being interactable while invisible, plus not displaying previous dates.
94 |
95 | ## 1.0.4 (2023-08-22)
96 | ### Bug Fixes
97 | - Adding missing label when displaying a `.custom()` Sign Up field.
98 |
99 | ## 1.0.3 (2023-08-17)
100 |
101 | ### Bug Fixes
102 | - Fixing wrong date format being submitted when displaying `SignUpField` of type `date` (See [#31](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/31))
103 |
104 | ## 1.0.2 (2023-07-25)
105 |
106 | ### Misc. Updates
107 | - Pinning the Amplify version up to 2.15.x
108 |
109 | ## 1.0.1 (2023-06-15)
110 |
111 | ### Bug Fixes
112 | - Fixing issues with Sign Up fields (See [#25](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/25)).
113 | - Removing duplicated fields in the array provided to `Authenticator.signUpFields(_:)`
114 | - Preventing fields of type `.phoneNumber` from saving an incomplete phone number if only the dialling code is set.
115 | - Fixing Xcode 15 beta compilation error (See [#24](https://github.com/aws-amplify/amplify-ui-swift-authenticator/pull/24), thanks @RowbotNZ!)
116 |
117 |
118 | ## 1.0.0 (2023-05-24)
119 |
120 | ### Initial release of Amplify UI Authenticator for Swift UI
121 |
122 | Amplify Authenticator provides a complete drop-in implementation of an authentication flow for your application using [Amplify Authentication](https://docs.amplify.aws/lib/auth/getting-started/q/platform/ios/).
123 |
124 | More information on setting up and using the component is in the [documentation](https://ui.docs.amplify.aws/swift/connected-components/authenticator).
125 |
--------------------------------------------------------------------------------