diff --git a/CODEOWNERS b/CODEOWNERS index 552a2d7da7..9ccd2bdaea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,7 @@ msal/src/main/java/com/microsoft/identity/nativeauth/ @AzureAD/NativeAuthTeam msal/src/test/java/com/microsoft/identity/nativeauth/ @AzureAD/NativeAuthTeam msal/src/androidTest/java/com/microsoft/identity/nativeauth/ @AzureAD/NativeAuthTeam +msal/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth @AzureAD/NativeAuthTeam # If you are interested in reviewing or getting notified of changes in a particular area # Please add your alias against that specific path below diff --git a/testapps/testapp/build.gradle b/testapps/testapp/build.gradle index d937c13f7a..529d52b173 100644 --- a/testapps/testapp/build.gradle +++ b/testapps/testapp/build.gradle @@ -25,7 +25,11 @@ apply plugin: 'com.android.application' -def msalVersion = "5.0.0" +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +def msalVersion = "5.+" if (project.hasProperty("distMsalVersion")) { msalVersion = distMsalVersion @@ -83,6 +87,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + buildFeatures { + viewBinding true + } lintOptions { abortOnError false } @@ -111,6 +118,8 @@ dependencies { localImplementation project(':msal') distImplementation "com.microsoft.identity.client:msal:${msalVersion}" implementation "com.google.code.gson:gson:$rootProject.ext.gsonVersion" + implementation "androidx.constraintlayout:constraintlayout:$rootProject.ext.constraintLayoutVersion" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.ext.kotlinXCoroutinesVersion" implementation "androidx.appcompat:appcompat:$rootProject.ext.appCompatVersion" implementation "androidx.legacy:legacy-support-v4:$rootProject.ext.legacySupportV4Version" implementation "com.google.android.material:material:$rootProject.ext.materialVersion" diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/Constants.java b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/Constants.java index 32976b69da..43e1f3584a 100644 --- a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/Constants.java +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/Constants.java @@ -113,4 +113,12 @@ public enum AuthScheme { BEARER, POP } + + /** + * Constants used in native auth flows. + */ + public static final String STATE = "state"; + public static final String CODE_LENGTH = "code_length"; + public static final String SENT_TO = "sent_to"; + public static final String CHANNEL = "channel"; } diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/MainActivity.java b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/MainActivity.java index bc83b8a04b..15791ff510 100644 --- a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/MainActivity.java +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/MainActivity.java @@ -208,6 +208,11 @@ public boolean onNavigationItemSelected(final MenuItem item) { return false; } fragment = new AcquireTokenFragment(); + } else if ( menuItemId == R.id.nav_native) { + if (getCurrentFragment() instanceof NativeAuthFragment){ + return false; + } + fragment = new NativeAuthFragment(); } else if (menuItemId == R.id.nav_result) { if (getCurrentFragment() instanceof ResultFragment){ return false; diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/NativeAuthFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/NativeAuthFragment.kt new file mode 100644 index 0000000000..f37bea0f50 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/NativeAuthFragment.kt @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.microsoft.identity.client.testapp.nativeauth.AuthClient +import com.microsoft.identity.client.testapp.nativeauth.EmailAttributeSignUpFragment +import com.microsoft.identity.client.testapp.nativeauth.EmailPasswordSignInSignUpFragment +import com.microsoft.identity.client.testapp.nativeauth.EmailSignInSignUpFragment +import com.microsoft.identity.client.testapp.nativeauth.PasswordResetFragment + +/** + * Fragment used for starting various native auth flows. + */ +class NativeAuthFragment : Fragment() { + companion object { + private val TAG = MainActivity::class.java.simpleName + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = inflater.inflate(R.layout.fragment_native, container, false) + + AuthClient.initialize(requireContext()) + + val emailSignInSignUpFragment = EmailSignInSignUpFragment() + val emailPasswordSignInSignUpFragment = EmailPasswordSignInSignUpFragment() + val emailAttributeSignUpFragment = EmailAttributeSignUpFragment() + val passwordResetFragment = PasswordResetFragment() + + val bottomNavigationView = view.findViewById(R.id.bottom_navigation_view) + + setFragment(emailSignInSignUpFragment, R.string.title_email_oob_sisu) + + bottomNavigationView.setOnNavigationItemSelectedListener { + when (it.itemId) { + R.id.email_oob_sisu -> setFragment(emailSignInSignUpFragment, R.string.title_email_oob_sisu) + R.id.email_password_sisu -> setFragment(emailPasswordSignInSignUpFragment, R.string.title_email_password_sisu) + R.id.email_attribute_sisu -> setFragment(emailAttributeSignUpFragment, R.string.title_email_attribute_oob_sisu) + R.id.email_oob_sspr -> setFragment(passwordResetFragment, R.string.title_email_oob_sspr) + } + true + } + + return view + } + + private fun setFragment(fragment: Fragment, title: Int) { + (this.context as AppCompatActivity).supportFragmentManager.beginTransaction().apply { + replace(R.id.scenario_fragment, fragment) + commit() + } + } +} \ No newline at end of file diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/AuthClient.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/AuthClient.kt new file mode 100644 index 0000000000..e648f91e77 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/AuthClient.kt @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.Application +import android.content.Context +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication + +/** + * Utility for managing MSAL SDK instance INativeAuthPublicClientApplication. + */ +object AuthClient : Application() { + private lateinit var authClient: INativeAuthPublicClientApplication + + fun getAuthClient(): INativeAuthPublicClientApplication { + return authClient + } + + fun initialize(context: Context) { + authClient = PublicClientApplication.createNativeAuthPublicClientApplication( + context, + R.raw.msal_config_native + ) + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailAttributeSignUpFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailAttributeSignUpFragment.kt new file mode 100644 index 0000000000..57ba63fa1f --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailAttributeSignUpFragment.kt @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentEmailAttributeBinding +import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication +import com.microsoft.identity.nativeauth.UserAttributes +import com.microsoft.identity.nativeauth.statemachine.errors.GetAccessTokenError +import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult +import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.AccountState +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpCodeRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for the email, password and user attributes sign up flow. + */ +class EmailAttributeSignUpFragment : Fragment() { + private lateinit var authClient: INativeAuthPublicClientApplication + private var _binding: FragmentEmailAttributeBinding? = null + private val binding get() = _binding!! + + companion object { + private enum class STATUS { SignedIn, SignedOut } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEmailAttributeBinding.inflate(inflater, container, false) + val view = binding.root + + authClient = AuthClient.getAuthClient() + + init() + + return view + } + + override fun onResume() { + super.onResume() + getStateAndUpdateUI() + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.signUp.setOnClickListener { + signUp() + } + + binding.signOut.setOnClickListener { + signOut() + } + } + + private fun getStateAndUpdateUI() { + CoroutineScope(Dispatchers.Main).launch { + val accountResult = authClient.getCurrentAccount() + when (accountResult) { + is GetAccountResult.AccountFound -> { + displaySignedInState(accountResult.resultValue) + } + is GetAccountResult.NoAccountFound -> { + displaySignedOutState() + } + } + } + } + private fun signUp() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + val password = CharArray(binding.passwordText.length()); + binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0); + + val attributes = UserAttributes.Builder + + val attr1Key = binding.attr1KeyText.text.toString() + if (attr1Key.isNotBlank()) { + val attr1Value = binding.attr1ValueText.toString() + attributes + .customAttribute(attr1Key, attr1Value) + } + + val attr2Key = binding.attr2KeyText.text.toString() + if (attr2Key.isNotBlank()) { + val attr2Value = binding.attr2ValueText.toString() + attributes + .customAttribute(attr2Key, attr2Value) + } + + val actionResult = authClient.signUp( + username = email, + password = password, + attributes = attributes.build() + ) + + password.fill('0'); + + when (actionResult) { + is SignUpResult.CodeRequired -> { + navigateToSignUp( + nextState = actionResult.nextState, + codeLength = actionResult.codeLength, + sentTo = actionResult.sentTo, + channel = actionResult.channel + ) + } + is SignUpResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_up_successful_message), + Toast.LENGTH_SHORT + ).show() + signInAfterSignUp( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog("Unexpected result", actionResult.toString()) + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn() + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedInState(accountState = actionResult.resultValue) + } + else -> { + displayDialog( "Unexpected result", actionResult.toString()) + } + } + } + private fun signOut() { + CoroutineScope(Dispatchers.Main).launch { + val getAccountResult = authClient.getCurrentAccount() + if (getAccountResult is GetAccountResult.AccountFound) { + val signOutResult = getAccountResult.resultValue.signOut() + if (signOutResult is SignOutResult.Complete) { + Toast.makeText( + requireContext(), + getString(R.string.sign_out_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedOutState() + } else { + displayDialog( "Unexpected result", signOutResult.toString()) + } + } + } + } + private fun displaySignedInState(accountState: AccountState) { + emptyFields() + updateUI(STATUS.SignedIn) + displayAccount(accountState) + } + + private fun displaySignedOutState() { + emptyFields() + updateUI(STATUS.SignedOut) + emptyResults() + } + + private fun updateUI(status: STATUS) { + when (status) { + STATUS.SignedIn -> { + binding.signUp.isEnabled = false + binding.signOut.isEnabled = true + } + STATUS.SignedOut -> { + binding.signUp.isEnabled = true + binding.signOut.isEnabled = false + } + } + } + + private fun emptyFields() { + binding.emailText.setText("") + binding.passwordText.setText("") + binding.attr1KeyText.setText("") + binding.attr1ValueText.setText("") + binding.attr2KeyText.setText("") + binding.attr2ValueText.setText("") + } + + private fun emptyResults() { + binding.resultAccessToken.text = "" + binding.resultIdToken.text = "" + } + + private fun displayAccount(accountState: AccountState) { + CoroutineScope(Dispatchers.Main).launch { + try { + val accessTokenResult = accountState.getAccessToken() + when (accessTokenResult) { + is GetAccessTokenResult.Complete -> { + binding.resultAccessToken.text = + getString(R.string.result_access_token_text) + accessTokenResult.resultValue.accessToken + + val idToken = accountState.getIdToken() + binding.resultIdToken.text = + getString(R.string.result_id_token_text) + idToken + } + + is GetAccessTokenError -> { + displayDialog( + getString(R.string.msal_exception_title), + accessTokenResult.exception?.message.toString() + ) + } + } + } catch (exception: Exception) { + displayDialog( + getString(R.string.msal_exception_title), + exception.message.toString() + ) + } + } + } + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + private fun navigateToSignUp( + nextState: SignUpCodeRequiredState, + codeLength: Int, + sentTo: String, + channel: String + ) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + bundle.putInt(Constants.CODE_LENGTH, codeLength) + bundle.putString(Constants.SENT_TO, sentTo) + bundle.putString(Constants.CHANNEL, channel) + val fragment = SignUpCodeFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailPasswordSignInSignUpFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailPasswordSignInSignUpFragment.kt new file mode 100644 index 0000000000..7ec74a4883 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailPasswordSignInSignUpFragment.kt @@ -0,0 +1,350 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.Account +import com.microsoft.identity.client.AcquireTokenParameters +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentEmailPasswordBinding +import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication +import com.microsoft.identity.nativeauth.statemachine.errors.SignInError +import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult +import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.AccountState +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpCodeRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for the email and password sign up and sign in flow. + */ +class EmailPasswordSignInSignUpFragment : Fragment() { + + private lateinit var authClient: INativeAuthPublicClientApplication + private var _binding: FragmentEmailPasswordBinding? = null + private val binding get() = _binding!! + + companion object { + private enum class STATUS { SignedIn, SignedOut } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentEmailPasswordBinding.inflate(inflater, container, false) + val view = binding.root + + authClient = AuthClient.getAuthClient() + + init() + + return view + } + + override fun onResume() { + super.onResume() + getStateAndUpdateUI() + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.signIn.setOnClickListener { + signIn() + } + + binding.signUp.setOnClickListener { + signUp() + } + + binding.signOut.setOnClickListener { + signOut() + } + } + private fun getStateAndUpdateUI() { + CoroutineScope(Dispatchers.Main).launch { + val accountResult = authClient.getCurrentAccount() + when (accountResult) { + is GetAccountResult.AccountFound -> { + displaySignedInState(accountResult.resultValue) + } + is GetAccountResult.NoAccountFound -> { + displaySignedOutState() + } + } + } + } + private fun signIn() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + val password = CharArray(binding.passwordText.length()); + binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0); + + val actionResult = authClient.signIn( + username = email, + password = password + ) + + password.fill('0'); + + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedInState(accountState = actionResult.resultValue) + } + is SignInError -> { + if (actionResult.isBrowserRequired()) { + Toast.makeText(requireContext(), actionResult.errorMessage, Toast.LENGTH_SHORT).show() + + authClient.acquireToken( + AcquireTokenParameters( + AcquireTokenParameters.Builder() + .startAuthorizationFromActivity(requireActivity()) + .withScopes(mutableListOf("profile", "openid", "email")) + .withCallback(getAuthInteractiveCallback()) + ) + ) + } + } + else -> { + displayDialog( "Unexpected result", actionResult.toString()) + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun signUp() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + val password = CharArray(binding.passwordText.length()); + binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0); + + val actionResult = authClient.signUp( + username = email, + password = password + ) + + password.fill('0') + + when (actionResult) { + is SignUpResult.CodeRequired -> { + navigateToSignUp( + nextState = actionResult.nextState, + codeLength = actionResult.codeLength, + sentTo = actionResult.sentTo, + channel = actionResult.channel + ) + } + is SignUpResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_up_successful_message), + Toast.LENGTH_SHORT + ).show() + signInAfterSignUp( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog("Unexpected result", actionResult.toString()) + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn() + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedInState(accountState = actionResult.resultValue) + } + else -> { + displayDialog("Unexpected result", actionResult.toString()) + } + } + } + private fun signOut() { + CoroutineScope(Dispatchers.Main).launch { + val getAccountResult = authClient.getCurrentAccount() + if (getAccountResult is GetAccountResult.AccountFound) { + val signOutResult = getAccountResult.resultValue.signOut() + if (signOutResult is SignOutResult.Complete) { + Toast.makeText( + requireContext(), + getString(R.string.sign_out_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedOutState() + } else { + displayDialog("Unexpected result", signOutResult.toString()) + } + } + } + } + private fun displaySignedInState(accountState: AccountState) { + emptyFields() + updateUI(STATUS.SignedIn) + displayAccount(accountState) + } + + private fun displaySignedOutState() { + emptyFields() + updateUI(STATUS.SignedOut) + emptyResults() + } + private fun updateUI(status: STATUS) { + when (status) { + STATUS.SignedIn -> { + binding.signIn.isEnabled = false + binding.signUp.isEnabled = false + binding.signOut.isEnabled = true + } + STATUS.SignedOut -> { + binding.signIn.isEnabled = true + binding.signUp.isEnabled = true + binding.signOut.isEnabled = false + } + } + } + + private fun emptyFields() { + binding.emailText.setText("") + binding.passwordText.setText("") + } + + private fun emptyResults() { + binding.resultAccessToken.text = "" + binding.resultIdToken.text = "" + } + private fun displayAccount(accountState: AccountState) { + CoroutineScope(Dispatchers.Main).launch { + val accessTokenState = accountState.getAccessToken() + if (accessTokenState is GetAccessTokenResult.Complete) { + val accessToken = accessTokenState.resultValue.accessToken + binding.resultAccessToken.text = + getString(R.string.result_access_token_text) + accessToken + + val idToken = accountState.getIdToken() + binding.resultIdToken.text = getString(R.string.result_id_token_text) + idToken + } + } + } + + private fun displayDialog(error: String? = null, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + + /** + * Callback used for interactive request. + * If succeeds we use the access token to call the Microsoft Graph. + * Does not check cache. + */ + private fun getAuthInteractiveCallback(): AuthenticationCallback { + return object : AuthenticationCallback { + + override fun onSuccess(authenticationResult: IAuthenticationResult) { + /* Successfully got a token, use it to call a protected resource - MSGraph */ + + val accountResult = authenticationResult.account as Account + + /* Update account */ + emptyFields() + updateUI(STATUS.SignedIn) + val idToken = accountResult.idToken + binding.resultIdToken.text = + getString(R.string.result_id_token_text) + idToken + + Toast.makeText(requireContext(), getString(R.string.sign_in_successful_message), Toast.LENGTH_SHORT).show() + } + + override fun onError(exception: MsalException) { + /* Failed to acquireToken */ + displayDialog(getString(R.string.msal_exception_title), exception.errorCode) + } + + override fun onCancel() { + /* User canceled the authentication */ + } + } + } + private fun navigateToSignUp( + nextState: SignUpCodeRequiredState, + codeLength: Int, + sentTo: String, + channel: String + ) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + bundle.putInt(Constants.CODE_LENGTH, codeLength) + bundle.putString(Constants.SENT_TO, sentTo) + bundle.putString(Constants.CHANNEL, channel) + val fragment = SignUpCodeFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailSignInSignUpFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailSignInSignUpFragment.kt new file mode 100644 index 0000000000..4f5ee93812 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/EmailSignInSignUpFragment.kt @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentEmailSisuBinding +import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication +import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult +import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.AccountState +import com.microsoft.identity.nativeauth.statemachine.states.SignInCodeRequiredState +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpCodeRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for the email sign up and sign in flow. + */ +class EmailSignInSignUpFragment : Fragment() { + private lateinit var authClient: INativeAuthPublicClientApplication + private var _binding: FragmentEmailSisuBinding? = null + private val binding get() = _binding!! + + companion object { + private enum class STATUS { SignedIn, SignedOut } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentEmailSisuBinding.inflate(inflater, container, false) + val view = binding.root + + authClient = AuthClient.getAuthClient() + + init() + + return view + } + + override fun onResume() { + super.onResume() + getStateAndUpdateUI() + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.signIn.setOnClickListener { + signIn() + } + + binding.signUp.setOnClickListener { + signUp() + } + + binding.signOut.setOnClickListener { + signOut() + } + } + private fun getStateAndUpdateUI() { + CoroutineScope(Dispatchers.Main).launch { + val accountResult = authClient.getCurrentAccount() + when (accountResult) { + is GetAccountResult.AccountFound -> { + displaySignedInState(accountResult.resultValue) + } + is GetAccountResult.NoAccountFound -> { + displaySignedOutState() + } + } + } + } + private fun signIn() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + + val actionResult = authClient.signIn( + username = email + ) + + when (actionResult) { + is SignInResult.CodeRequired -> { + navigateToSignIn( + signInstate = actionResult.nextState, + codeLength = actionResult.codeLength, + sentTo = actionResult.sentTo, + channel = actionResult.channel + ) + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun signUp() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + + val actionResult = authClient.signUp( + username = email + ) + + when (actionResult) { + is SignUpResult.CodeRequired -> { + navigateToSignUp( + nextState = actionResult.nextState, + codeLength = actionResult.codeLength, + sentTo = actionResult.sentTo, + channel = actionResult.channel + ) + } + is SignUpResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_up_successful_message), + Toast.LENGTH_SHORT + ).show() + signInAfterSignUp( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn() + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedInState(accountState = actionResult.resultValue) + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } + private fun signOut() { + CoroutineScope(Dispatchers.Main).launch { + val getAccountResult = authClient.getCurrentAccount() + if (getAccountResult is GetAccountResult.AccountFound) { + val signOutResult = getAccountResult.resultValue.signOut() + if (signOutResult is SignOutResult.Complete) { + Toast.makeText( + requireContext(), + getString(R.string.sign_out_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedOutState() + } else { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $signOutResult") + } + } + } + } + private fun displaySignedInState(accountState: AccountState) { + emptyFields() + updateUI(STATUS.SignedIn) + displayAccount(accountState) + } + + private fun displaySignedOutState() { + emptyFields() + updateUI(STATUS.SignedOut) + emptyResults() + } + + private fun updateUI(status: STATUS) { + when (status) { + STATUS.SignedIn -> { + binding.signIn.isEnabled = false + binding.signUp.isEnabled = false + binding.signOut.isEnabled = true + } + STATUS.SignedOut -> { + binding.signIn.isEnabled = true + binding.signUp.isEnabled = true + binding.signOut.isEnabled = false + } + } + } + + private fun emptyFields() { + binding.emailText.setText(/* text = */ "") + } + + private fun emptyResults() { + binding.resultAccessToken.text = "" + binding.resultIdToken.text = "" + } + private fun displayAccount(accountState: AccountState) { + CoroutineScope(Dispatchers.Main).launch { + val accessTokenState = accountState.getAccessToken() + if (accessTokenState is GetAccessTokenResult.Complete) { + val accessToken = accessTokenState.resultValue.accessToken + binding.resultAccessToken.text = + getString(R.string.result_access_token_text) + accessToken + + val idToken = accountState.getIdToken() + binding.resultIdToken.text = getString(R.string.result_id_token_text) + idToken + } + } + } + private fun displayDialog(error: String? = null, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + + private fun navigateToSignIn( + signInstate: SignInCodeRequiredState, + codeLength: Int, + sentTo: String, + channel: String + ) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, signInstate) + bundle.putInt(Constants.CODE_LENGTH, codeLength) + bundle.putString(Constants.SENT_TO, sentTo) + bundle.putString(Constants.CHANNEL, channel) + val fragment = SignInCodeFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } + + private fun navigateToSignUp( + nextState: SignUpCodeRequiredState, + codeLength: Int, + sentTo: String, + channel: String + ) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + bundle.putInt(Constants.CODE_LENGTH, codeLength) + bundle.putString(Constants.SENT_TO, sentTo) + bundle.putString(Constants.CHANNEL, channel) + val fragment = SignUpCodeFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetCodeFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetCodeFragment.kt new file mode 100644 index 0000000000..f4804a7c74 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetCodeFragment.kt @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentCodeBinding +import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResendCodeResult +import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordSubmitCodeResult +import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordCodeRequiredState +import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordPasswordRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for managing the code step in the reset password flow. + */ +class PasswordResetCodeFragment : Fragment() { + private lateinit var currentState: ResetPasswordCodeRequiredState + private var _binding: FragmentCodeBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentCodeBinding.inflate(inflater, container, false) + val view = binding.root + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as ResetPasswordCodeRequiredState + + init() + + return view + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.verifyCode.setOnClickListener { + submitCode() + } + + binding.resendCodeText.setOnClickListener { + resendCode() + } + } + + private fun submitCode() { + CoroutineScope(Dispatchers.Main).launch { + try { + val code = binding.codeText.text.toString() + + val actionResult = currentState.submitCode(code) + + when (actionResult) { + is ResetPasswordSubmitCodeResult.PasswordRequired -> { + navigateToResetPasswordPasswordFragment( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun resendCode() { + clearCode() + + CoroutineScope(Dispatchers.Main).launch { + val actionResult = currentState.resendCode() + + when (actionResult) { + is ResetPasswordResendCodeResult.Success -> { + currentState = actionResult.nextState + Toast.makeText(requireContext(), getString(R.string.resend_code_message), Toast.LENGTH_LONG).show() + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } + } + + private fun clearCode() { + binding.codeText.text?.clear() + } + + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + + private fun navigateToResetPasswordPasswordFragment(nextState: ResetPasswordPasswordRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = PasswordResetNewPasswordFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetFragment.kt new file mode 100644 index 0000000000..bdf502552c --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetFragment.kt @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentEmailSsprBinding +import com.microsoft.identity.nativeauth.INativeAuthPublicClientApplication +import com.microsoft.identity.nativeauth.statemachine.results.GetAccessTokenResult +import com.microsoft.identity.nativeauth.statemachine.results.GetAccountResult +import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordStartResult +import com.microsoft.identity.nativeauth.statemachine.results.SignOutResult +import com.microsoft.identity.nativeauth.statemachine.states.AccountState +import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordCodeRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for initiating the reset password flow. + */ +class PasswordResetFragment : Fragment() { + private lateinit var authClient: INativeAuthPublicClientApplication + private var _binding: FragmentEmailSsprBinding? = null + private val binding get() = _binding!! + + companion object { + private enum class STATUS { SignedIn, SignedOut } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentEmailSsprBinding.inflate(inflater, container, false) + val view = binding.root + + authClient = AuthClient.getAuthClient() + + init() + + return view + } + + override fun onResume() { + super.onResume() + getStateAndUpdateUI() + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.forgetPassword.setOnClickListener { + forgetPassword() + } + + binding.signOut.setOnClickListener { + signOut() + } + } + private fun getStateAndUpdateUI() { + CoroutineScope(Dispatchers.Main).launch { + val accountResult = authClient.getCurrentAccount() + when (accountResult) { + is GetAccountResult.AccountFound -> { + displaySignedInState(accountResult.resultValue) + } + is GetAccountResult.NoAccountFound -> { + displaySignedOutState() + } + } + } + } + private fun forgetPassword() { + CoroutineScope(Dispatchers.Main).launch { + try { + val email = binding.emailText.text.toString() + + val actionResult = authClient.resetPassword( + username = email + ) + when (actionResult) { + is ResetPasswordStartResult.CodeRequired -> { + navigateToResetPasswordCodeFragment( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog(getString(R.string.msal_exception_title), "Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun signOut() { + CoroutineScope(Dispatchers.Main).launch { + val getAccountResult = authClient.getCurrentAccount() + if (getAccountResult is GetAccountResult.AccountFound) { + val signOutResult = getAccountResult.resultValue.signOut() + if (signOutResult is SignOutResult.Complete) { + Toast.makeText( + requireContext(), + getString(R.string.sign_out_successful_message), + Toast.LENGTH_SHORT + ).show() + displaySignedOutState() + } else { + displayDialog("Unexpected result", signOutResult.toString()) + } + } + } + } + private fun displaySignedInState(accountState: AccountState) { + emptyFields() + updateUI(STATUS.SignedIn) + displayAccount(accountState) + } + private fun displaySignedOutState() { + emptyFields() + updateUI(STATUS.SignedOut) + emptyResults() + } + + private fun updateUI(status: STATUS) { + when (status) { + STATUS.SignedIn -> { + binding.forgetPassword.isEnabled = false + binding.signOut.isEnabled = true + } + STATUS.SignedOut -> { + binding.forgetPassword.isEnabled = true + binding.signOut.isEnabled = false + } + } + } + + private fun emptyFields() { + binding.emailText.setText("") + } + + private fun emptyResults() { + binding.resultAccessToken.text = "" + binding.resultIdToken.text = "" + } + + private fun displayAccount(accountState: AccountState) { + CoroutineScope(Dispatchers.Main).launch { + val accessTokenState = accountState.getAccessToken() + if (accessTokenState is GetAccessTokenResult.Complete) { + val accessToken = accessTokenState.resultValue.accessToken + binding.resultAccessToken.text = + getString(R.string.result_access_token_text) + accessToken + + val idToken = accountState.getIdToken() + binding.resultIdToken.text = getString(R.string.result_id_token_text) + idToken + } + } + } + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + private fun navigateToResetPasswordCodeFragment(nextState: ResetPasswordCodeRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = PasswordResetCodeFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetNewPasswordFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetNewPasswordFragment.kt new file mode 100644 index 0000000000..69a5f95700 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/PasswordResetNewPasswordFragment.kt @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentPasswordBinding +import com.microsoft.identity.nativeauth.statemachine.results.ResetPasswordResult +import com.microsoft.identity.nativeauth.statemachine.states.ResetPasswordPasswordRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for setting the new password in the reset password flow. + */ +class PasswordResetNewPasswordFragment : Fragment() { + private lateinit var currentState: ResetPasswordPasswordRequiredState + private var _binding: FragmentPasswordBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentPasswordBinding.inflate(inflater, container, false) + val view = binding.root + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as ResetPasswordPasswordRequiredState + + init() + + return view + } + + private fun init() { + initializeButtonListener() + } + + private fun initializeButtonListener() { + binding.submitPassword.setOnClickListener { + resetPassword() + } + } + + private fun resetPassword() { + CoroutineScope(Dispatchers.Main).launch { + try { + val password = CharArray(binding.passwordText.length()); + binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0); + + val actionResult = currentState.submitPassword(password) + + password.fill('0') + + when (actionResult) { + is ResetPasswordResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.password_reset_success_message), + Toast.LENGTH_LONG + ).show() + finish() + } + else -> { + displayDialog(getString(R.string.msal_exception_title),"Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + private fun finish() { + val fragmentManager = requireActivity().supportFragmentManager + val name: String? = fragmentManager.getBackStackEntryAt(0).name + fragmentManager.popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignInCodeFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignInCodeFragment.kt new file mode 100644 index 0000000000..2cea2f3779 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignInCodeFragment.kt @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentCodeBinding +import com.microsoft.identity.nativeauth.statemachine.results.SignInResendCodeResult +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.states.SignInCodeRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for managing the code step in the sign in flow. + */ +class SignInCodeFragment : Fragment() { + private lateinit var currentState: SignInCodeRequiredState + private var codeLength: Int? = null + private var sentTo: String? = null + private var channel: String? = null + private var _binding: FragmentCodeBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentCodeBinding.inflate(inflater, container, false) + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as SignInCodeRequiredState + codeLength = bundle.getInt(Constants.CODE_LENGTH) + sentTo = bundle.getString(Constants.SENT_TO) + channel = bundle.getString(Constants.CHANNEL) + + init() + + return binding.root + } + + private fun init() { + initializeButtonListeners() + binding.codeHint.text = "Code sent to ${sentTo}, by ${channel}, with length ${codeLength}}" + } + + private fun initializeButtonListeners() { + binding.verifyCode.setOnClickListener { + verifyCode() + } + + binding.resendCodeText.setOnClickListener { + resendCode() + } + } + + private fun verifyCode() { + CoroutineScope(Dispatchers.Main).launch { + try { + val emailCode = binding.codeText.text.toString() + + val actionResult = currentState.submitCode(emailCode) + + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + finish() + } + else -> { + displayDialog(getString(R.string.msal_exception_title),"Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + + private fun resendCode() { + clearCode() + + CoroutineScope(Dispatchers.Main).launch { + val actionResult = currentState.resendCode() + + when (actionResult) { + is SignInResendCodeResult.Success -> { + currentState = actionResult.nextState + Toast.makeText(requireContext(), getString(R.string.resend_code_message), Toast.LENGTH_LONG).show() + } + else -> { + displayDialog(getString(R.string.msal_exception_title),"Unexpected result: $actionResult") + } + } + } + } + + private fun clearCode() { + binding.codeText.text?.clear() + } + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + private fun finish() { + requireActivity().supportFragmentManager.popBackStackImmediate() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpAttributesFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpAttributesFragment.kt new file mode 100644 index 0000000000..4fd3bfbf52 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpAttributesFragment.kt @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentAttributeBinding +import com.microsoft.identity.nativeauth.UserAttributes +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpAttributesRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for submitting attributes in the sign up flow. This Fragment is used in a scenario + * where attributes are submitted after setting the code and/or password. + */ +class SignUpAttributesFragment : Fragment() { + private lateinit var currentState: SignUpAttributesRequiredState + private var _binding: FragmentAttributeBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentAttributeBinding.inflate(inflater, container, false) + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as SignUpAttributesRequiredState + + init() + + return binding.root + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.submitAttributes.setOnClickListener { + create() + } + } + + private fun create() { + CoroutineScope(Dispatchers.Main).launch { + try { + val attributes = UserAttributes.Builder + + val attr1Key = binding.attr1KeyText.text.toString() + if (attr1Key.isNotBlank()) { + val attr1Value = binding.attr1ValueText.toString() + attributes + .customAttribute(attr1Key, attr1Value) + } + + val attr2Key = binding.attr2KeyText.text.toString() + if (attr2Key.isNotBlank()) { + val attr2Value = binding.attr2ValueText.toString() + attributes + .customAttribute(attr2Key, attr2Value) + } + + val actionResult = currentState.submitAttributes(attributes.build()) + + when (actionResult) { + is SignUpResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_up_successful_message), + Toast.LENGTH_SHORT + ).show() + signInAfterSignUp(actionResult.nextState) + } + is SignUpResult.AttributesRequired -> { + navigateToAttributes( + nextState = actionResult.nextState + ) + } + else -> { + displayDialog(getString(R.string.msal_exception_title),"Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayDialog(getString(R.string.msal_exception_title), exception.message.toString()) + } + } + } + private fun displayDialog(error: String?, message: String?) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(error) + .setMessage(message) + val alertDialog = builder.create() + alertDialog.show() + } + + private fun navigateToAttributes(nextState: SignUpAttributesRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = SignUpAttributesFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn(null) + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + finish() + } + else -> { + displayDialog(getString(R.string.msal_exception_title),"Unexpected result: $actionResult") + } + } + } + + private fun finish() { + val fragmentManager = requireActivity().supportFragmentManager + val name: String? = fragmentManager.getBackStackEntryAt(0).name + fragmentManager.popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpCodeFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpCodeFragment.kt new file mode 100644 index 0000000000..5ae35588b6 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpCodeFragment.kt @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentCodeBinding +import com.microsoft.identity.nativeauth.statemachine.errors.SubmitCodeError +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResendCodeResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpAttributesRequiredState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpCodeRequiredState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpPasswordRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for managing the code step in the sign up flow. + */ +class SignUpCodeFragment : Fragment() { + private lateinit var currentState: SignUpCodeRequiredState + private var codeLength: Int? = null + private var sentTo: String? = null + private var channel: String? = null + private var _binding: FragmentCodeBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentCodeBinding.inflate(inflater, container, false) + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as SignUpCodeRequiredState + codeLength = bundle.getInt(Constants.CODE_LENGTH) + sentTo = bundle.getString(Constants.SENT_TO) + channel = bundle.getString(Constants.CHANNEL) + + init() + + return binding.root + } + + private fun init() { + initializeButtonListeners() + binding.codeHint.text = "Code sent to ${sentTo}, by ${channel}, with length ${codeLength}}" + } + + private fun initializeButtonListeners() { + binding.verifyCode.setOnClickListener { + verifyCode() + } + + binding.resendCodeText.setOnClickListener { + resendCode() + } + } + + private fun verifyCode() { + CoroutineScope(Dispatchers.Main).launch { + try { + val oobCode = binding.codeText.text.toString() + + val actionResult = currentState.submitCode(oobCode) + + when (actionResult) { + is SignUpResult.Complete -> { + Toast.makeText(requireContext(), getString(R.string.sign_up_successful_message), Toast.LENGTH_SHORT).show() + signInAfterSignUp( + nextState = actionResult.nextState + ) + } + is SubmitCodeError -> { + if (actionResult.isInvalidCode()) { + Toast.makeText( + requireContext(), + actionResult.errorMessage, + Toast.LENGTH_SHORT + ).show() + clearCode() + } else { + displayError("Unexpected result: $actionResult") + } + } + is SignUpResult.AttributesRequired -> { + navigateToAttributes( + nextState = actionResult.nextState + ) + } + is SignUpResult.PasswordRequired -> { + navigateToPassword( + nextState = actionResult.nextState + ) + } + else -> { + displayError("Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayError(exception.message.toString()) + } + } + } + + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn(null) + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + finish() + } + else -> { + displayError("Unexpected result: $actionResult") + } + } + } + + private fun resendCode() { + clearCode() + + CoroutineScope(Dispatchers.Main).launch { + try { + val actionResult = currentState.resendCode() + + when (actionResult) { + is SignUpResendCodeResult.Success -> { + currentState = actionResult.nextState + Toast.makeText(requireContext(), getString(R.string.resend_code_message), Toast.LENGTH_LONG).show() + } + else -> { + displayError("Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayError(exception.message.toString()) + } + } + } + + private fun clearCode() { + binding.codeText.text?.clear() + } + + private fun displayError(errorMsg: String) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(getString(R.string.msal_exception_title)) + .setMessage(errorMsg) + val alertDialog = builder.create() + alertDialog.show() + } + + private fun finish() { + requireActivity().supportFragmentManager.popBackStackImmediate() + } + + private fun navigateToAttributes(nextState: SignUpAttributesRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = SignUpAttributesFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } + + private fun navigateToPassword(nextState: SignUpPasswordRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = SignUpPasswordFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpPasswordFragment.kt b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpPasswordFragment.kt new file mode 100644 index 0000000000..64de344fc8 --- /dev/null +++ b/testapps/testapp/src/main/java/com/microsoft/identity/client/testapp/nativeauth/SignUpPasswordFragment.kt @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.client.testapp.nativeauth + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.microsoft.identity.client.exception.MsalException +import com.microsoft.identity.client.testapp.Constants +import com.microsoft.identity.client.testapp.R +import com.microsoft.identity.client.testapp.databinding.FragmentPasswordBinding +import com.microsoft.identity.nativeauth.statemachine.results.SignInResult +import com.microsoft.identity.nativeauth.statemachine.results.SignUpResult +import com.microsoft.identity.nativeauth.statemachine.states.SignInContinuationState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpAttributesRequiredState +import com.microsoft.identity.nativeauth.statemachine.states.SignUpPasswordRequiredState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Fragment used for managing the password in the sign up flow. + */ +class SignUpPasswordFragment : Fragment() { + private lateinit var currentState: SignUpPasswordRequiredState + private var _binding: FragmentPasswordBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentPasswordBinding.inflate(inflater, container, false) + + val bundle = this.arguments + currentState = bundle!!.getSerializable(Constants.STATE) as SignUpPasswordRequiredState + + init() + + return binding.root + } + + private fun init() { + initializeButtonListeners() + } + + private fun initializeButtonListeners() { + binding.submitPassword.setOnClickListener { + submitPassword() + } + } + + private fun submitPassword() { + CoroutineScope(Dispatchers.Main).launch { + try { + val password = CharArray(binding.passwordText.length()); + binding.passwordText.text?.getChars(0, binding.passwordText.length(), password, 0); + + val actionResult = currentState.submitPassword(password) + + password.fill('0'); + + when (actionResult) { + is SignUpResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_up_successful_message), + Toast.LENGTH_SHORT + ).show() + signInAfterSignUp(actionResult.nextState) + } + is SignUpResult.AttributesRequired -> { + navigateToAttributes( + actionResult.nextState + ) + } + else -> { + displayError("Unexpected result: $actionResult") + } + } + } catch (exception: MsalException) { + displayError(exception.message.toString()) + } + } + } + + private fun displayError(errorMsg: String) { + val builder = AlertDialog.Builder(requireContext()) + builder.setTitle(getString(R.string.msal_exception_title)) + .setMessage(errorMsg) + val alertDialog = builder.create() + alertDialog.show() + } + + private suspend fun signInAfterSignUp(nextState: SignInContinuationState) { + val currentState = nextState + val actionResult = currentState.signIn(null) + when (actionResult) { + is SignInResult.Complete -> { + Toast.makeText( + requireContext(), + getString(R.string.sign_in_successful_message), + Toast.LENGTH_SHORT + ).show() + finish() + } + else -> { + displayError("Unexpected result: $actionResult") + } + } + } + + private fun finish() { + val fragmentManager = requireActivity().supportFragmentManager + val name: String? = fragmentManager.getBackStackEntryAt(0).name + fragmentManager.popBackStack(name, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + + private fun navigateToAttributes(nextState: SignUpAttributesRequiredState) { + val bundle = Bundle() + bundle.putParcelable(Constants.STATE, nextState) + val fragment = SignUpAttributesFragment() + fragment.arguments = bundle + + requireActivity().supportFragmentManager + .beginTransaction() + .setReorderingAllowed(true) + .addToBackStack(fragment::class.java.name) + .replace(R.id.scenario_fragment, fragment) + .commit() + } +} diff --git a/testapps/testapp/src/main/res/drawable/ic_baseline_email_24.xml b/testapps/testapp/src/main/res/drawable/ic_baseline_email_24.xml new file mode 100644 index 0000000000..a92c2dfd88 --- /dev/null +++ b/testapps/testapp/src/main/res/drawable/ic_baseline_email_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/testapps/testapp/src/main/res/drawable/ic_baseline_lock_reset_24.xml b/testapps/testapp/src/main/res/drawable/ic_baseline_lock_reset_24.xml new file mode 100644 index 0000000000..3bac2e66c8 --- /dev/null +++ b/testapps/testapp/src/main/res/drawable/ic_baseline_lock_reset_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/testapps/testapp/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml b/testapps/testapp/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml new file mode 100644 index 0000000000..fc32883937 --- /dev/null +++ b/testapps/testapp/src/main/res/drawable/ic_baseline_remove_red_eye_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/testapps/testapp/src/main/res/drawable/ic_baseline_text_snippet_24.xml b/testapps/testapp/src/main/res/drawable/ic_baseline_text_snippet_24.xml new file mode 100644 index 0000000000..7913f6d909 --- /dev/null +++ b/testapps/testapp/src/main/res/drawable/ic_baseline_text_snippet_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/testapps/testapp/src/main/res/drawable/side_nav_bar.xml b/testapps/testapp/src/main/res/drawable/side_nav_bar.xml index bbb8b0a6c6..8829a0f6f4 100644 --- a/testapps/testapp/src/main/res/drawable/side_nav_bar.xml +++ b/testapps/testapp/src/main/res/drawable/side_nav_bar.xml @@ -6,4 +6,4 @@ android:endColor="#2e367d" android:startColor="#8188c7" android:type="linear" /> - \ No newline at end of file + diff --git a/testapps/testapp/src/main/res/layout/fragment_attribute.xml b/testapps/testapp/src/main/res/layout/fragment_attribute.xml new file mode 100644 index 0000000000..b86c044ef1 --- /dev/null +++ b/testapps/testapp/src/main/res/layout/fragment_attribute.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +