// // Copyright Amazon.com Inc. or its affiliates. // All Rights Reserved. // // SPDX-License-Identifier: Apache-2.0 // import Amplify import SwiftUI /// The Authenticator component public struct Authenticator<LoadingContent: View, SignInContent: View, ConfirmSignInWithNewPasswordContent: View, ConfirmSignInWithMFACodeContent: View, ConfirmSignInWithCustomChallengeContent: View, SignUpContent: View, ConfirmSignUpContent: View, ResetPasswordContent: View, ConfirmResetPasswordContent: View, VerifyUserContent: View, ConfirmVerifyUserContent: View, SignedInContent: View, ErrorContent: View, Header: View, Footer: View>: View { @Environment(\.authenticationService) var authenticationService @Environment(\.authenticatorState) var state @State private var currentStep: Step = .loading @State private var previousStep: Step = .loading private var initialStep: AuthenticatorInitialStep private var viewModifiers = ViewModifiers() private var contentStates: NSHashTable<AuthenticatorBaseState> = .weakObjects() private let loadingContent: LoadingContent private let signInContent: SignInContent private let confirmSignInContentWithMFACodeContent: ConfirmSignInWithMFACodeContent private let confirmSignInContentWithCustomChallengeContent: ConfirmSignInWithCustomChallengeContent private let confirmSignInContentWithNewPasswordContent: ConfirmSignInWithNewPasswordContent private let signUpContent: SignUpContent private let confirmSignUpContent: ConfirmSignUpContent private let resetPasswordContent: ResetPasswordContent private let confirmResetPasswordContent: ConfirmResetPasswordContent private let verifyUserContent: VerifyUserContent private let confirmVerifyUserContent: ConfirmVerifyUserContent private let headerContent: Header private let footerContent: Footer private let errorContentBuilder: (Error) -> ErrorContent private let signedInContentBuilder: (SignedInState) -> SignedInContent /// Creates an `Authenticator` component /// - Parameter initialStep: The initial step displayed to unauthorized users. /// Defaults to ``AuthenticatorInitialStep/signIn`` /// - Parameter loadingContent: The content that is associated with the ``AuthenticatorStep/loading`` step. /// Defaults to a `SwiftUI.ProgressView`. /// - Parameter signInContent: The content associated with the ``AuthenticatorStep/signIn`` step. /// Defaults to a ``SignInView``. /// - Parameter confirmSignInWithMFACodeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithCustomChallenge`` step. /// Defaults to a ``ConfirmSignInWithMFACodeView``. /// - Parameter confirmSignInWithCustomChallengeContent: The content associated with the ``AuthenticatorStep/confirmSignInWithMFACode`` step. /// Defaults to a ``ConfirmSignInWithCustomChallengeView``. /// - Parameter confirmSignInWithNewPasswordContent: The content associated with the ``AuthenticatorStep/confirmSignInWithNewPassword`` step. /// Defaults to a ``ConfirmSignInWithNewPasswordView``. /// - Parameter signUpContent: The content associated with the ``AuthenticatorStep/signUp`` step. /// Defaults to a ``SignUpView``. /// - Parameter confirmSignUpContent: The content associated with the ``AuthenticatorStep/confirmSignUp`` step. /// Defaults to a ``ConfirmSignUpView``. /// - Parameter resetPasswordContent: The content associated with the ``AuthenticatorStep/resetPassword`` step. /// Defaults to a ``ResetPasswordView``. /// - Parameter confirmResetPasswordContent: The content associated with the ``AuthenticatorStep/confirmResetPassword`` step. /// Defaults to a ``ConfirmResetPasswordView``. /// - Parameter verifyUserContent: The content associated with the ``AuthenticatorStep/verifyUser`` step. /// Defaults to a ``VerifyUserView``. /// - Parameter confirmVerifyUserContent: The content associated with the ``AuthenticatorStep/confirmVerifyUser`` step. /// Defaults to a ``ConfirmVerifyUserView``. /// - Parameter errorContent: The content associated with the ``AuthenticatorStep/error`` step. /// Defaults to a ``ErrorView``. /// - Parameter headerContent: A custom header content that is displayed on top of any other Authenticator content. /// Defaults to a `SwiftUI.EmptyView`. /// - Parameter footerContent: A custom footer content that is displayed below any other Authenticator content. /// Defaults to a `SwiftUI.EmptyView`. /// - Parameter content: The content associated with the ``AuthenticatorStep/signedIn`` step, i.e. once the user has successfully authenticated. public init( initialStep: AuthenticatorInitialStep = .signIn, @ViewBuilder loadingContent: () -> LoadingContent = { ProgressView() }, @ViewBuilder signInContent: (SignInState) -> SignInContent = { state in SignInView(state: state) }, @ViewBuilder confirmSignInWithMFACodeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithMFACodeContent = { state in ConfirmSignInWithMFACodeView(state: state) }, @ViewBuilder confirmSignInWithCustomChallengeContent: (ConfirmSignInWithCodeState) -> ConfirmSignInWithCustomChallengeContent = { state in ConfirmSignInWithCustomChallengeView(state: state) }, @ViewBuilder confirmSignInWithNewPasswordContent: (ConfirmSignInWithNewPasswordState) -> ConfirmSignInWithNewPasswordContent = { state in ConfirmSignInWithNewPasswordView(state: state) }, @ViewBuilder signUpContent: (SignUpState) -> SignUpContent = { state in SignUpView(state: state) }, @ViewBuilder confirmSignUpContent: (ConfirmSignUpState) -> ConfirmSignUpContent = { state in ConfirmSignUpView(state: state) }, @ViewBuilder resetPasswordContent: (ResetPasswordState) -> ResetPasswordContent = { state in ResetPasswordView(state: state) }, @ViewBuilder confirmResetPasswordContent: (ConfirmResetPasswordState) -> ConfirmResetPasswordContent = { state in ConfirmResetPasswordView(state: state) }, @ViewBuilder verifyUserContent: (VerifyUserState) -> VerifyUserContent = { state in VerifyUserView(state: state) }, @ViewBuilder confirmVerifyUserContent: (ConfirmVerifyUserState) -> ConfirmVerifyUserContent = { state in ConfirmVerifyUserView(state: state) }, @ViewBuilder errorContent: @escaping (Error) -> ErrorContent = { _ in ErrorView() }, @ViewBuilder headerContent: () -> Header = { EmptyView() }, @ViewBuilder footerContent: () -> Footer = { EmptyView() }, @ViewBuilder content: @escaping (SignedInState) -> SignedInContent ) { self.initialStep = initialStep self.loadingContent = loadingContent() let credentials = Credentials() let signInState = SignInState(credentials: credentials) contentStates.add(signInState) self.signInContent = signInContent(signInState) let confirmSignInWithMFACodeState = ConfirmSignInWithCodeState(credentials: credentials) contentStates.add(confirmSignInWithMFACodeState) self.confirmSignInContentWithMFACodeContent = confirmSignInWithMFACodeContent( confirmSignInWithMFACodeState ) let confirmSignInWithCustomChallengeState = ConfirmSignInWithCodeState(credentials: credentials) contentStates.add(confirmSignInWithMFACodeState) self.confirmSignInContentWithCustomChallengeContent = confirmSignInWithCustomChallengeContent( confirmSignInWithCustomChallengeState ) let confirmSignInWithNewPasswordState = ConfirmSignInWithNewPasswordState(credentials: credentials) contentStates.add(confirmSignInWithNewPasswordState) self.confirmSignInContentWithNewPasswordContent = confirmSignInWithNewPasswordContent( confirmSignInWithNewPasswordState ) let signUpState = SignUpState(credentials: credentials) contentStates.add(signUpState) self.signUpContent = signUpContent(signUpState) let confirmSignUpState = ConfirmSignUpState(credentials: credentials) contentStates.add(confirmSignUpState) self.confirmSignUpContent = confirmSignUpContent(confirmSignUpState) let resetPasswordState = ResetPasswordState(credentials: credentials) contentStates.add(resetPasswordState) self.resetPasswordContent = resetPasswordContent(resetPasswordState) let confirmResetPasswordState = ConfirmResetPasswordState(credentials: credentials) contentStates.add(confirmResetPasswordState) self.confirmResetPasswordContent = confirmResetPasswordContent(confirmResetPasswordState) let verifyUserState = VerifyUserState(credentials: credentials) contentStates.add(verifyUserState) self.verifyUserContent = verifyUserContent(verifyUserState) let confirmVerifyUserState = ConfirmVerifyUserState(credentials: credentials) contentStates.add(confirmVerifyUserState) self.confirmVerifyUserContent = confirmVerifyUserContent(confirmVerifyUserState) self.headerContent = headerContent() self.footerContent = footerContent() self.errorContentBuilder = errorContent self.signedInContentBuilder = content } public var body: some View { VStack { Group { if case .signedIn(let user) = currentStep { let signedInState = SignedInState( user: user, authenticationService: authenticationService ) signedInContentBuilder(signedInState) .environmentObject(signedInState) } else { headerContent createView(for: currentStep) footerContent } } .transition(contentTransition) } .animation(viewModifiers.contentAnimation, value: currentStep) .environment(\.authenticatorOptions.hidesSignUpButton, viewModifiers.hidesSignUpButton) .environment(\.authenticatorOptions.contentAnimation, viewModifiers.contentAnimation) .environment(\.authenticatorOptions.contentTransition, viewModifiers.contentTransition) .environment(\.authenticatorOptions.signUpFields, viewModifiers.signUpFields) .environment(\.authenticatorOptions.busyStyle, viewModifiers.busyStyle) .task { state.authenticationService = authenticationService setUpContentStates(contentStates) await state.reloadState(initialStep: initialStep) } .onChange(of: contentStates) { contentStates in setUpContentStates(contentStates) } .onChange(of: initialStep) { initialStep in Task { await state.reloadState(initialStep: initialStep) } } .onReceive(state.$step) { self.previousStep = self.currentStep self.currentStep = $0 } } /// Hides the Sign Up Button that is displayed in the default ``SignInView``. /// - Parameter hidesSignUpButton: Whether to hide the Sign Up button. Defaults to true public func hidesSignUpButton(_ hidesSignUpButton: Bool = true) -> Self { var view = self view.viewModifiers.hidesSignUpButton = hidesSignUpButton return view } /// Sets the animation used to transition between steps. /// - Parameter animation: The animation that will be used to apply UI changes when the Authenticator's current step changes. public func contentAnimation(_ animation: Animation) -> Self { var view = self view.viewModifiers.contentAnimation = animation return view } /// Sets the transition used to transition between steps. /// - Parameter transition: The transition that will be used to apply UI changes when the Authenticator's current step changes. public func contentTransition(_ transition: AnyTransition) -> Self { var view = self view.viewModifiers.contentTransition = transition return view } /// Sets the Sign Up fields that will be displayed in the `.signUp` step. /// - Parameter signUpFields: An array of Sign Up fields that will be displayed when signing up. The order of the array is mantained when displaying the fields. public func signUpFields(_ signUpFields: [SignUpField]) -> Self { var view = self view.viewModifiers.signUpFields = signUpFields return view } /// Sets a custom error mapping function for the `AuthError`s that are displayed /// - Parameter errorTransform: A closure that takes an `AuthError` and returns a ``AuthenticatorError`` that will be displayed. public func errorMap(_ errorTransform: @escaping (AuthError) -> AuthenticatorError) -> Self { for contentState in contentStates.allObjects { contentState.errorTransform = errorTransform } return self } /// Sets the style that is applied when an operation is in progress /// - Parameter blurRadius: The radial size that determines how diffuse the blur behind the content is. public func busyStyle(blurRadius: CGFloat) -> Self { var view = self view.viewModifiers.busyStyle.blurRadius = blurRadius return view } /// Sets the style that is applied when an operation is in progress /// - Parameter blurRadius: The radial size that determines how diffuse the blur behind the content is. /// Defaults to `nil`, which keeps the existing value. /// - Parameter content: A closure that returns the content that is displayed while an operation is in progress public func busyStyle<Content: View>(blurRadius: CGFloat? = nil, content: () -> Content) -> Self { var view = self view.viewModifiers.busyStyle.content = content() if let blurRadius = blurRadius { view.viewModifiers.busyStyle.blurRadius = blurRadius } return view } /// Sets a custom theme /// - Parameter theme: A theme that will be applied to the Authenticator and all its views public func authenticatorTheme(_ theme: AuthenticatorTheme) -> some View { environment(\.authenticatorTheme, theme) } func authenticationService(_ authenticationService: AuthenticationService) -> some View { environment(\.authenticationService, authenticationService) } @ViewBuilder private func createView(for step: Step) -> some View { switch step { case .loading: loadingContent case .signIn: signInContent case .confirmSignInWithNewPassword: confirmSignInContentWithNewPasswordContent case .confirmSignInWithMFACode: confirmSignInContentWithMFACodeContent case .confirmSignInWithCustomChallenge: confirmSignInContentWithCustomChallengeContent case .resetPassword: resetPasswordContent case .confirmResetPassword: confirmResetPasswordContent case .signUp: signUpContent case .confirmSignUp: confirmSignUpContent case .verifyUser: verifyUserContent case .confirmVerifyUser: confirmVerifyUserContent case .error(let error): errorContentBuilder(error) case .signedIn(_): // Should never happen EmptyView() } } private var contentTransition: AnyTransition { if previousStep == .loading { return .opacity } return viewModifiers.contentTransition } private func setUpContentStates(_ contentStates: NSHashTable<AuthenticatorBaseState>) { for contentState in contentStates.allObjects { contentState.configure(with: state) } } } extension Authenticator { private struct ViewModifiers { var hidesSignUpButton = false var contentAnimation: Animation = .easeInOut(duration: 0.25) var contentTransition: AnyTransition = .opacity var signUpFields: [SignUpField] = [] var busyStyle = AuthenticatorOptions.BusyStyle(content: ProgressView()) } }