From f89de9e9897528b86ff5adeb194926e0536bfeac Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Wed, 20 May 2026 12:10:36 +0530 Subject: [PATCH 1/5] feat: add My Account API support for managing MFA authentication method --- auth0_flutter/README.md | 60 +++++ .../Auth0FlutterMyAccountMethodCallHandler.kt | 30 +++ .../auth0/auth0_flutter/Auth0FlutterPlugin.kt | 19 ++ .../MyAccountExceptionExtensions.kt | 13 + .../auth0_flutter/MyAccountExtensions.kt | 61 +++++ ...eleteAuthenticationMethodRequestHandler.kt | 46 ++++ .../my_account/EnrollEmailRequestHandler.kt | 50 ++++ .../my_account/EnrollPhoneRequestHandler.kt | 58 +++++ .../my_account/EnrollPushRequestHandler.kt | 45 ++++ .../EnrollRecoveryCodeRequestHandler.kt | 47 ++++ .../my_account/EnrollTotpRequestHandler.kt | 45 ++++ .../GetAuthenticationMethodRequestHandler.kt | 44 ++++ .../GetAuthenticationMethodsRequestHandler.kt | 43 ++++ .../my_account/GetFactorsRequestHandler.kt | 43 ++++ .../my_account/MyAccountRequestHandler.kt | 10 + .../my_account/VerifyOtpRequestHandler.kt | 51 ++++ .../auth0_flutter/Auth0FlutterPluginTest.kt | 8 +- ...h0FlutterMyAccountMethodCallHandlerTest.kt | 68 ++++++ ...eAuthenticationMethodRequestHandlerTest.kt | 94 +++++++ .../EnrollEmailRequestHandlerTest.kt | 99 ++++++++ .../EnrollPhoneRequestHandlerTest.kt | 141 +++++++++++ .../EnrollPushRequestHandlerTest.kt | 84 +++++++ .../EnrollRecoveryCodeRequestHandlerTest.kt | 84 +++++++ .../EnrollTotpRequestHandlerTest.kt | 84 +++++++ ...tAuthenticationMethodRequestHandlerTest.kt | 100 ++++++++ ...AuthenticationMethodsRequestHandlerTest.kt | 85 +++++++ .../GetFactorsRequestHandlerTest.kt | 91 +++++++ .../my_account/VerifyOtpRequestHandlerTest.kt | 142 +++++++++++ ...AccountDeleteAuthMethodMethodHandler.swift | 29 +++ .../MyAccountEnrollEmailMethodHandler.swift | 29 +++ .../MyAccountEnrollPhoneMethodHandler.swift | 34 +++ .../MyAccountEnrollPushMethodHandler.swift | 25 ++ ...countEnrollRecoveryCodeMethodHandler.swift | 25 ++ .../MyAccountEnrollTotpMethodHandler.swift | 25 ++ .../MyAccountAPI/MyAccountExtensions.swift | 103 ++++++++ .../MyAccountGetAuthMethodMethodHandler.swift | 29 +++ ...MyAccountGetAuthMethodsMethodHandler.swift | 25 ++ .../MyAccountGetFactorsMethodHandler.swift | 25 ++ .../MyAccountAPI/MyAccountHandler.swift | 86 +++++++ .../MyAccountVerifyOtpMethodHandler.swift | 35 +++ .../Classes/SwiftAuth0FlutterPlugin.swift | 3 +- auth0_flutter/lib/auth0_flutter.dart | 43 +++- .../lib/src/mobile/my_account_api.dart | 230 ++++++++++++++++++ .../lib/auth0_flutter_platform_interface.dart | 17 ++ .../auth0_flutter_my_account_platform.dart | 84 +++++++ .../src/myaccount/authentication_method.dart | 57 +++++ .../src/myaccount/enrollment_challenge.dart | 36 +++ .../lib/src/myaccount/factor.dart | 19 ++ ...thod_channel_auth0_flutter_my_account.dart | 171 +++++++++++++ ...my_account_delete_auth_method_options.dart | 17 ++ .../my_account_enroll_email_options.dart | 17 ++ .../my_account_enroll_phone_options.dart | 21 ++ .../my_account_enroll_push_options.dart | 12 + ..._account_enroll_recovery_code_options.dart | 12 + .../my_account_enroll_totp_options.dart | 12 + .../src/myaccount/my_account_exception.dart | 38 +++ .../my_account_get_auth_method_options.dart | 17 ++ .../my_account_get_auth_methods_options.dart | 12 + .../my_account_get_factors_options.dart | 12 + .../my_account_verify_otp_options.dart | 23 ++ .../lib/src/myaccount/phone_type.dart | 24 ++ 61 files changed, 3080 insertions(+), 12 deletions(-) create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterMyAccountMethodCallHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/MyAccountRequestHandler.kt create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/Auth0FlutterMyAccountMethodCallHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountHandler.swift create mode 100644 auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift create mode 100644 auth0_flutter/lib/src/mobile/my_account_api.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/enrollment_challenge.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/factor.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_delete_auth_method_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_email_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_phone_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_push_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_recovery_code_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_totp_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_method_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_methods_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_factors_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/my_account_verify_otp_options.dart create mode 100644 auth0_flutter_platform_interface/lib/src/myaccount/phone_type.dart diff --git a/auth0_flutter/README.md b/auth0_flutter/README.md index c9d9aa800..197f79541 100644 --- a/auth0_flutter/README.md +++ b/auth0_flutter/README.md @@ -474,6 +474,66 @@ void dispose() { - [clearCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/DefaultCredentialsManager/clearCredentials.html) - [ssoCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/DefaultCredentialsManager/ssoCredentials.html) +#### My Account API + +The My Account API allows users to manage their own multi-factor authentication (MFA) methods. Available on **mobile (Android/iOS) only**. + +First, authenticate with the `https://{domain}/me/` audience: + +```dart +final credentials = await auth0.webAuthentication().login( + audience: 'https://YOUR_DOMAIN/me/', + scopes: { + 'openid', + 'read:me:authentication_methods', + 'create:me:authentication_methods', + 'delete:me:authentication_methods', + 'read:me:enrollments', + 'read:me:factors', + }, +); +``` + +Then create the My Account client and use it: + +```dart +final myAccount = auth0.myAccount(accessToken: credentials.accessToken); + +// List enrolled MFA methods +final methods = await myAccount.getAuthenticationMethods(); + +// List available factors +final factors = await myAccount.getFactors(); + +// Enroll a new phone factor +final challenge = await myAccount.enrollPhone( + phoneNumber: '+1234567890', + type: PhoneType.sms, +); + +// Verify enrollment with OTP +await myAccount.verifyOtp( + id: challenge.id, + authSession: challenge.authSession, + otp: '123456', +); + +// Delete a method +await myAccount.deleteAuthenticationMethod(id: 'method_id'); +``` + +Other enrollment methods: `enrollEmail`, `enrollTotp`, `enrollPush`, `enrollRecoveryCode`. + +Error handling: + +```dart +try { + await myAccount.getAuthenticationMethods(); +} on MyAccountException catch (e) { + print('${e.code}: ${e.message} (${e.statusCode})'); +} +``` + ### 🌐 Web - [loginWithRedirect](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter_web/Auth0Web/loginWithRedirect.html) diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterMyAccountMethodCallHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterMyAccountMethodCallHandler.kt new file mode 100644 index 000000000..0a9f615be --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterMyAccountMethodCallHandler.kt @@ -0,0 +1,30 @@ +package com.auth0.auth0_flutter + +import android.content.Context +import androidx.annotation.NonNull +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.request_handlers.my_account.MyAccountRequestHandler +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class Auth0FlutterMyAccountMethodCallHandler( + private val myAccountRequestHandlers: List +) : MethodCallHandler { + lateinit var context: Context + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + val request = MethodCallRequest.fromCall(call) + + val handler = myAccountRequestHandlers.find { it.method == call.method } + if (handler != null) { + val accessToken = request.data["accessToken"] as? String ?: "" + val client = MyAccountAPIClient(request.account, accessToken) + + handler.handle(client, request, result) + } else { + result.notImplemented() + } + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt index 9709a47aa..a983d6d07 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/Auth0FlutterPlugin.kt @@ -8,6 +8,7 @@ import com.auth0.android.result.Credentials import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import com.auth0.auth0_flutter.request_handlers.api.* import com.auth0.auth0_flutter.request_handlers.credentials_manager.* +import com.auth0.auth0_flutter.request_handlers.my_account.* import com.auth0.auth0_flutter.request_handlers.web_auth.LoginWebAuthRequestHandler import com.auth0.auth0_flutter.request_handlers.web_auth.LogoutWebAuthRequestHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -27,6 +28,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private lateinit var authMethodChannel : MethodChannel private lateinit var credentialsManagerMethodChannel : MethodChannel private lateinit var dpopMethodChannel : MethodChannel + private lateinit var myAccountMethodChannel : MethodChannel private lateinit var binding: FlutterPlugin.FlutterPluginBinding private lateinit var authCallHandler: Auth0FlutterAuthMethodCallHandler private var pendingRecoveredCredentials: Map? = null @@ -49,6 +51,18 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { GetDPoPHeadersApiRequestHandler(), ClearDPoPKeyApiRequestHandler() )) + private val myAccountCallHandler = Auth0FlutterMyAccountMethodCallHandler(listOf( + GetAuthenticationMethodsRequestHandler(), + GetAuthenticationMethodRequestHandler(), + DeleteAuthenticationMethodRequestHandler(), + GetFactorsRequestHandler(), + EnrollPhoneRequestHandler(), + EnrollEmailRequestHandler(), + EnrollTotpRequestHandler(), + EnrollPushRequestHandler(), + EnrollRecoveryCodeRequestHandler(), + VerifyOtpRequestHandler() + )) private val processDeathCallback = object : Callback { override fun onSuccess(credentials: Credentials) { @@ -125,6 +139,10 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { dpopMethodChannel = MethodChannel(messenger, "auth0.com/auth0_flutter/dpop") dpopMethodChannel.setMethodCallHandler(dpopCallHandler) + + myAccountMethodChannel = MethodChannel(messenger, "auth0.com/auth0_flutter/my_account") + myAccountMethodChannel.setMethodCallHandler(myAccountCallHandler) + myAccountCallHandler.context = context } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { @@ -150,6 +168,7 @@ class Auth0FlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { authMethodChannel.setMethodCallHandler(null) credentialsManagerMethodChannel.setMethodCallHandler(null) dpopMethodChannel.setMethodCallHandler(null) + myAccountMethodChannel.setMethodCallHandler(null) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt new file mode 100644 index 000000000..9409b178c --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt @@ -0,0 +1,13 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.myaccount.MyAccountException + +fun MyAccountException.toMyAccountMap(): Map { + val exception = this + return buildMap { + put("_statusCode", exception.statusCode) + put("_errorFlags", mapOf( + "isNetworkError" to exception.isNetworkError, + )) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt new file mode 100644 index 000000000..47661076e --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt @@ -0,0 +1,61 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.result.AuthenticationMethod +import com.auth0.android.result.EmailAuthenticationMethod +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.android.result.MfaAuthenticationMethod +import com.auth0.android.result.MfaEnrollmentChallenge +import com.auth0.android.result.PhoneAuthenticationMethod +import com.auth0.android.result.PushNotificationAuthenticationMethod +import com.auth0.android.result.RecoveryCodeEnrollmentChallenge +import com.auth0.android.result.TotpAuthenticationMethod +import com.auth0.android.result.TotpEnrollmentChallenge + +fun AuthenticationMethod.toMyAccountMethodMap(): Map { + return buildMap { + put("id", id) + put("type", type) + put("created_at", createdAt) + when (val method = this@toMyAccountMethodMap) { + is PhoneAuthenticationMethod -> { + put("name", method.name) + put("phone_number", method.phoneNumber) + put("preferred_authentication_method", + method.preferredAuthenticationMethod) + } + is EmailAuthenticationMethod -> { + put("name", method.name) + put("email", method.email) + } + is TotpAuthenticationMethod -> { + put("name", method.name) + } + is PushNotificationAuthenticationMethod -> { + put("name", method.name) + } + else -> {} + } + if (this@toMyAccountMethodMap is MfaAuthenticationMethod) { + put("confirmed", confirmed) + } + } +} + +fun EnrollmentChallenge.toMyAccountChallengeMap(): Map { + return buildMap { + put("id", id) + put("auth_session", authSession) + when (val challenge = this@toMyAccountChallengeMap) { + is TotpEnrollmentChallenge -> { + put("barcode_uri", challenge.barcodeUri) + put("totp_secret", challenge.manualInputCode) + put("totp_uri", challenge.manualInputCode) + } + is RecoveryCodeEnrollmentChallenge -> { + put("recovery_code", challenge.recoveryCode) + } + is MfaEnrollmentChallenge -> {} + else -> {} + } + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandler.kt new file mode 100644 index 000000000..094ab494c --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandler.kt @@ -0,0 +1,46 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_DELETE_AUTH_METHOD_METHOD = + "myAccount#deleteAuthenticationMethod" + +class DeleteAuthenticationMethodRequestHandler : + MyAccountRequestHandler { + override val method: String = + MY_ACCOUNT_DELETE_AUTH_METHOD_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + assertHasProperties(listOf("id"), request.data) + + val id = request.data["id"] as String + + client.deleteAuthenticationMethod(id) + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess(res: Void?) { + result.success(null) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandler.kt new file mode 100644 index 000000000..1581d83cc --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandler.kt @@ -0,0 +1,50 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountChallengeMap +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_ENROLL_EMAIL_METHOD = + "myAccount#enrollEmail" + +class EnrollEmailRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_ENROLL_EMAIL_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + assertHasProperties(listOf("email"), request.data) + + val email = request.data["email"] as String + + client.enrollEmail(email) + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: EnrollmentChallenge + ) { + result.success( + res.toMyAccountChallengeMap() + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandler.kt new file mode 100644 index 000000000..3ef762c9a --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandler.kt @@ -0,0 +1,58 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.myaccount.PhoneAuthenticationMethodType +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountChallengeMap +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_ENROLL_PHONE_METHOD = + "myAccount#enrollPhone" + +class EnrollPhoneRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_ENROLL_PHONE_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + assertHasProperties( + listOf("phoneNumber", "type"), request.data + ) + + val phoneNumber = request.data["phoneNumber"] as String + val typeString = request.data["type"] as String + val type = when (typeString) { + "voice" -> PhoneAuthenticationMethodType.VOICE + else -> PhoneAuthenticationMethodType.SMS + } + + client.enrollPhone(phoneNumber, type) + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: EnrollmentChallenge + ) { + result.success( + res.toMyAccountChallengeMap() + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandler.kt new file mode 100644 index 000000000..a4b70ae67 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandler.kt @@ -0,0 +1,45 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.TotpEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountChallengeMap +import com.auth0.auth0_flutter.toMyAccountMap +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_ENROLL_PUSH_METHOD = + "myAccount#enrollPush" + +class EnrollPushRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_ENROLL_PUSH_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + client.enrollPushNotification() + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: TotpEnrollmentChallenge + ) { + result.success( + res.toMyAccountChallengeMap() + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandler.kt new file mode 100644 index 000000000..26d9500d1 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandler.kt @@ -0,0 +1,47 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.RecoveryCodeEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountChallengeMap +import com.auth0.auth0_flutter.toMyAccountMap +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_ENROLL_RECOVERY_CODE_METHOD = + "myAccount#enrollRecoveryCode" + +class EnrollRecoveryCodeRequestHandler : MyAccountRequestHandler { + override val method: String = + MY_ACCOUNT_ENROLL_RECOVERY_CODE_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + client.enrollRecoveryCode() + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: RecoveryCodeEnrollmentChallenge + ) { + result.success( + res.toMyAccountChallengeMap() + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandler.kt new file mode 100644 index 000000000..3ff8ce3a7 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandler.kt @@ -0,0 +1,45 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.TotpEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountChallengeMap +import com.auth0.auth0_flutter.toMyAccountMap +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_ENROLL_TOTP_METHOD = + "myAccount#enrollTotp" + +class EnrollTotpRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_ENROLL_TOTP_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + client.enrollTotp() + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: TotpEnrollmentChallenge + ) { + result.success( + res.toMyAccountChallengeMap() + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandler.kt new file mode 100644 index 000000000..f1f55ca19 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandler.kt @@ -0,0 +1,44 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.toMyAccountMethodMap +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_GET_AUTH_METHOD_METHOD = + "myAccount#getAuthenticationMethod" + +class GetAuthenticationMethodRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_GET_AUTH_METHOD_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + assertHasProperties(listOf("id"), request.data) + + val id = request.data["id"] as String + + client.getAuthenticationMethodById(id) + .start(object : + Callback { + override fun onFailure(exception: MyAccountException) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess(res: AuthenticationMethod) { + result.success(res.toMyAccountMethodMap()) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandler.kt new file mode 100644 index 000000000..e287fcf5c --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandler.kt @@ -0,0 +1,43 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.toMyAccountMethodMap +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_GET_AUTH_METHODS_METHOD = + "myAccount#getAuthenticationMethods" + +class GetAuthenticationMethodsRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_GET_AUTH_METHODS_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + client.getAuthenticationMethods() + .start(object : + Callback, MyAccountException> { + override fun onFailure(exception: MyAccountException) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: List + ) { + result.success( + res.map { it.toMyAccountMethodMap() } + ) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt new file mode 100644 index 000000000..6d336447f --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt @@ -0,0 +1,43 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.Factor +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountMap +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_GET_FACTORS_METHOD = + "myAccount#getFactors" + +class GetFactorsRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_GET_FACTORS_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + client.getFactors() + .start(object : + Callback, MyAccountException> { + override fun onFailure(exception: MyAccountException) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess(res: List) { + result.success(res.map { + mapOf( + "name" to it.type, + "enabled" to true + ) + }) + } + }) + } +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/MyAccountRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/MyAccountRequestHandler.kt new file mode 100644 index 000000000..01cc7e54f --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/MyAccountRequestHandler.kt @@ -0,0 +1,10 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel + +interface MyAccountRequestHandler { + val method: String + fun handle(client: MyAccountAPIClient, request: MethodCallRequest, result: MethodChannel.Result) +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt new file mode 100644 index 000000000..575240d6e --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt @@ -0,0 +1,51 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.utils.assertHasProperties +import io.flutter.plugin.common.MethodChannel + +private const val MY_ACCOUNT_VERIFY_OTP_METHOD = + "myAccount#verifyOtp" + +class VerifyOtpRequestHandler : MyAccountRequestHandler { + override val method: String = MY_ACCOUNT_VERIFY_OTP_METHOD + + override fun handle( + client: MyAccountAPIClient, + request: MethodCallRequest, + result: MethodChannel.Result + ) { + assertHasProperties( + listOf("id", "authSession", "otp"), request.data + ) + + val id = request.data["id"] as String + val authSession = request.data["authSession"] as String + val otp = request.data["otp"] as String + + client.verifyOtp(id, otp, authSession) + .start(object : + Callback { + override fun onFailure( + exception: MyAccountException + ) { + result.error( + exception.getCode(), + exception.getDescription(), + exception.toMyAccountMap() + ) + } + + override fun onSuccess( + res: AuthenticationMethod + ) { + result.success(null) + } + }) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt index 3a53c6560..6a4ea3b3e 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/Auth0FlutterPluginTest.kt @@ -38,8 +38,9 @@ class Auth0FlutterPluginTest { assertMethodcallHandler(1) assertMethodcallHandler(2) assertMethodcallHandler(3) + assertMethodcallHandler(4) - assert(constructed.size == 4) + assert(constructed.size == 5) } } @@ -69,8 +70,9 @@ class Auth0FlutterPluginTest { assertMethodcallHandler(1) assertMethodcallHandler(2) assertMethodcallHandler(3) + assertMethodcallHandler(4) - assert(constructed.size == 4) + assert(constructed.size == 5) } } @@ -108,7 +110,7 @@ class Auth0FlutterPluginTest { assert(getHandler(1).activity == mockActivity) assert(getHandler(1).context == mockContext) - assert(constructed.size == 4) + assert(constructed.size == 5) } } } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/Auth0FlutterMyAccountMethodCallHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/Auth0FlutterMyAccountMethodCallHandlerTest.kt new file mode 100644 index 000000000..11b77fc38 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/Auth0FlutterMyAccountMethodCallHandlerTest.kt @@ -0,0 +1,68 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.auth0_flutter.Auth0FlutterMyAccountMethodCallHandler +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class Auth0FlutterMyAccountMethodCallHandlerTest { + private val defaultArguments = hashMapOf( + "_account" to mapOf( + "domain" to "test.auth0.com", + "clientId" to "test-client", + ), + "_userAgent" to mapOf( + "name" to "auth0-flutter", + "version" to "1.0.0" + ), + "accessToken" to "test-token" + ) + + @Test + fun `handler should result in notImplemented if no matching handler`() { + val handler = Auth0FlutterMyAccountMethodCallHandler(emptyList()) + val mockResult = mock() + + handler.onMethodCall(MethodCall("myAccount#unknown", defaultArguments), mockResult) + + verify(mockResult).notImplemented() + } + + @Test + fun `handler should call the correct handler when matched`() { + val mockRequestHandler = mock() + `when`(mockRequestHandler.method).thenReturn("myAccount#getFactors") + + val handler = Auth0FlutterMyAccountMethodCallHandler(listOf(mockRequestHandler)) + val mockResult = mock() + + handler.onMethodCall(MethodCall("myAccount#getFactors", defaultArguments), mockResult) + + verify(mockRequestHandler).handle(any(), any(), eq(mockResult)) + } + + @Test + fun `handler should not call non-matching handlers`() { + val getFactorsHandler = mock() + val enrollPhoneHandler = mock() + + `when`(getFactorsHandler.method).thenReturn("myAccount#getFactors") + `when`(enrollPhoneHandler.method).thenReturn("myAccount#enrollPhone") + + val handler = Auth0FlutterMyAccountMethodCallHandler(listOf(getFactorsHandler, enrollPhoneHandler)) + val mockResult = mock() + + handler.onMethodCall(MethodCall("myAccount#getFactors", defaultArguments), mockResult) + + verify(getFactorsHandler).handle(any(), any(), eq(mockResult)) + verify(enrollPhoneHandler, times(0)).handle(any(), any(), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandlerTest.kt new file mode 100644 index 000000000..71ff72d96 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/DeleteAuthenticationMethodRequestHandlerTest.kt @@ -0,0 +1,94 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeleteAuthenticationMethodRequestHandlerTest { + + @Test + fun `should call deleteAuthenticationMethod with the correct id`() { + val handler = DeleteAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.deleteAuthenticationMethod(any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).deleteAuthenticationMethod(eq("phone|test123")) + } + + @Test + fun `should call result success with null on success`() { + val handler = DeleteAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.deleteAuthenticationMethod(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(null) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(isNull()) + } + + @Test + fun `should call result error on failure`() { + val handler = DeleteAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + val exception = mock() + + whenever(exception.getCode()).thenReturn("not_found") + whenever(exception.getDescription()).thenReturn("Method not found") + whenever(exception.statusCode).thenReturn(404) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.deleteAuthenticationMethod(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("not_found"), eq("Method not found"), any()) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when id is missing`() { + val handler = DeleteAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf() + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt new file mode 100644 index 000000000..dc2c7bc6e --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollEmailRequestHandlerTest.kt @@ -0,0 +1,99 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EnrollEmailRequestHandlerTest { + + @Test + fun `should call enrollEmail with the correct email`() { + val handler = EnrollEmailRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("email" to "test@example.com") + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.enrollEmail(any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollEmail(eq("test@example.com")) + } + + @Test + fun `should call result success with challenge map on success`() { + val handler = EnrollEmailRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("email" to "test@example.com") + val request = MethodCallRequest(account = mockAccount, options) + + val mockChallenge = mock() + whenever(mockChallenge.id).thenReturn("email|test123") + whenever(mockChallenge.authSession).thenReturn("session123") + + whenever(mockClient.enrollEmail(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockChallenge) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = EnrollEmailRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("email" to "test@example.com") + val request = MethodCallRequest(account = mockAccount, options) + val exception = mock() + + whenever(exception.getCode()).thenReturn("invalid_email") + whenever(exception.getDescription()).thenReturn("Invalid email") + whenever(exception.statusCode).thenReturn(400) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.enrollEmail(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("invalid_email"), eq("Invalid email"), any()) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when email is missing`() { + val handler = EnrollEmailRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf() + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt new file mode 100644 index 000000000..0bd649074 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPhoneRequestHandlerTest.kt @@ -0,0 +1,141 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.myaccount.PhoneAuthenticationMethodType +import com.auth0.android.request.Request +import com.auth0.android.result.EnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EnrollPhoneRequestHandlerTest { + + @Test + fun `should call enrollPhone with sms type`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "phoneNumber" to "+1234567890", + "type" to "sms" + ) + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.enrollPhone(any(), any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollPhone(eq("+1234567890"), eq(PhoneAuthenticationMethodType.SMS)) + } + + @Test + fun `should call enrollPhone with voice type`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "phoneNumber" to "+1234567890", + "type" to "voice" + ) + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.enrollPhone(any(), any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollPhone(eq("+1234567890"), eq(PhoneAuthenticationMethodType.VOICE)) + } + + @Test + fun `should call result success with challenge map on success`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "phoneNumber" to "+1234567890", + "type" to "sms" + ) + val request = MethodCallRequest(account = mockAccount, options) + + val mockChallenge = mock() + whenever(mockChallenge.id).thenReturn("phone|test123") + whenever(mockChallenge.authSession).thenReturn("session123") + + whenever(mockClient.enrollPhone(any(), any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockChallenge) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "phoneNumber" to "+1234567890", + "type" to "sms" + ) + val request = MethodCallRequest(account = mockAccount, options) + val exception = mock() + + whenever(exception.getCode()).thenReturn("invalid_phone") + whenever(exception.getDescription()).thenReturn("Invalid phone number") + whenever(exception.statusCode).thenReturn(400) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.enrollPhone(any(), any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("invalid_phone"), eq("Invalid phone number"), any()) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when phoneNumber is missing`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf("type" to "sms") + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when type is missing`() { + val handler = EnrollPhoneRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf("phoneNumber" to "+1234567890") + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandlerTest.kt new file mode 100644 index 000000000..6e604d721 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollPushRequestHandlerTest.kt @@ -0,0 +1,84 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.TotpEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EnrollPushRequestHandlerTest { + + @Test + fun `should call enrollPushNotification on the client`() { + val handler = EnrollPushRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + whenever(mockClient.enrollPushNotification()).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollPushNotification() + } + + @Test + fun `should call result success with challenge map on success`() { + val handler = EnrollPushRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + val mockChallenge = mock() + whenever(mockChallenge.id).thenReturn("push|test123") + whenever(mockChallenge.authSession).thenReturn("session123") + + whenever(mockClient.enrollPushNotification()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockChallenge) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = EnrollPushRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + val exception = mock() + + whenever(exception.getCode()).thenReturn("server_error") + whenever(exception.getDescription()).thenReturn("Server error") + whenever(exception.statusCode).thenReturn(500) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.enrollPushNotification()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("server_error"), eq("Server error"), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandlerTest.kt new file mode 100644 index 000000000..07867aad7 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollRecoveryCodeRequestHandlerTest.kt @@ -0,0 +1,84 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.RecoveryCodeEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EnrollRecoveryCodeRequestHandlerTest { + + @Test + fun `should call enrollRecoveryCode on the client`() { + val handler = EnrollRecoveryCodeRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + whenever(mockClient.enrollRecoveryCode()).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollRecoveryCode() + } + + @Test + fun `should call result success with challenge map on success`() { + val handler = EnrollRecoveryCodeRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + val mockChallenge = mock() + whenever(mockChallenge.id).thenReturn("recovery-code|test123") + whenever(mockChallenge.authSession).thenReturn("session123") + + whenever(mockClient.enrollRecoveryCode()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockChallenge) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = EnrollRecoveryCodeRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + val exception = mock() + + whenever(exception.getCode()).thenReturn("server_error") + whenever(exception.getDescription()).thenReturn("Server error") + whenever(exception.statusCode).thenReturn(500) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.enrollRecoveryCode()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("server_error"), eq("Server error"), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandlerTest.kt new file mode 100644 index 000000000..8241c4218 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/EnrollTotpRequestHandlerTest.kt @@ -0,0 +1,84 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.TotpEnrollmentChallenge +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class EnrollTotpRequestHandlerTest { + + @Test + fun `should call enrollTotp on the client`() { + val handler = EnrollTotpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + whenever(mockClient.enrollTotp()).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).enrollTotp() + } + + @Test + fun `should call result success with challenge map on success`() { + val handler = EnrollTotpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + val mockChallenge = mock() + whenever(mockChallenge.id).thenReturn("totp|test123") + whenever(mockChallenge.authSession).thenReturn("session123") + + whenever(mockClient.enrollTotp()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockChallenge) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = EnrollTotpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + val exception = mock() + + whenever(exception.getCode()).thenReturn("server_error") + whenever(exception.getDescription()).thenReturn("Server error") + whenever(exception.statusCode).thenReturn(500) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.enrollTotp()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("server_error"), eq("Server error"), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt new file mode 100644 index 000000000..0b4fa2483 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodRequestHandlerTest.kt @@ -0,0 +1,100 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GetAuthenticationMethodRequestHandlerTest { + + @Test + fun `should call getAuthenticationMethodById with the correct id`() { + val handler = GetAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.getAuthenticationMethodById(any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).getAuthenticationMethodById(eq("phone|test123")) + } + + @Test + fun `should call result success with mapped method on success`() { + val handler = GetAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + + val mockMethod = mock() + whenever(mockMethod.id).thenReturn("phone|test123") + whenever(mockMethod.type).thenReturn("phone") + whenever(mockMethod.createdAt).thenReturn("2026-01-01T00:00:00.000Z") + + whenever(mockClient.getAuthenticationMethodById(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockMethod) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = GetAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf("id" to "phone|test123") + val request = MethodCallRequest(account = mockAccount, options) + val exception = mock() + + whenever(exception.getCode()).thenReturn("not_found") + whenever(exception.getDescription()).thenReturn("Not found") + whenever(exception.statusCode).thenReturn(404) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.getAuthenticationMethodById(any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("not_found"), eq("Not found"), any()) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when id is missing`() { + val handler = GetAuthenticationMethodRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf() + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt new file mode 100644 index 000000000..87a84dd52 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetAuthenticationMethodsRequestHandlerTest.kt @@ -0,0 +1,85 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GetAuthenticationMethodsRequestHandlerTest { + + @Test + fun `should call getAuthenticationMethods on the client`() { + val handler = GetAuthenticationMethodsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + whenever(mockClient.getAuthenticationMethods()).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).getAuthenticationMethods() + } + + @Test + fun `should call result success with mapped methods on success`() { + val handler = GetAuthenticationMethodsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + val mockMethod = mock() + whenever(mockMethod.id).thenReturn("test-id") + whenever(mockMethod.type).thenReturn("phone") + whenever(mockMethod.createdAt).thenReturn("2026-01-01T00:00:00.000Z") + + whenever(mockClient.getAuthenticationMethods()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument, MyAccountException>>(0) + callback.onSuccess(listOf(mockMethod)) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(any()) + } + + @Test + fun `should call result error on failure`() { + val handler = GetAuthenticationMethodsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + val exception = mock() + + whenever(exception.getCode()).thenReturn("insufficient_scope") + whenever(exception.getDescription()).thenReturn("Insufficient scope") + whenever(exception.statusCode).thenReturn(403) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.getAuthenticationMethods()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument, MyAccountException>>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("insufficient_scope"), eq("Insufficient scope"), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt new file mode 100644 index 000000000..5c98ed09f --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt @@ -0,0 +1,91 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.Factor +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class GetFactorsRequestHandlerTest { + + @Test + fun `should call getFactors on the client`() { + val handler = GetFactorsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + whenever(mockClient.getFactors()).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).getFactors() + } + + @Test + fun `should call result success with mapped factors on success`() { + val handler = GetFactorsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + + val mockFactor = mock() + whenever(mockFactor.type).thenReturn("sms") + + whenever(mockClient.getFactors()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument, MyAccountException>>(0) + callback.onSuccess(listOf(mockFactor)) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + val captor = argumentCaptor>>() + verify(mockResult).success(captor.capture()) + + val result = captor.firstValue + assertThat(result.size, equalTo(1)) + assertThat(result[0]["name"], equalTo("sms")) + assertThat(result[0]["enabled"], equalTo(true)) + } + + @Test + fun `should call result error on failure`() { + val handler = GetFactorsRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock, MyAccountException>>() + val request = MethodCallRequest(account = mockAccount, hashMapOf()) + val exception = mock() + + whenever(exception.getCode()).thenReturn("access_denied") + whenever(exception.getDescription()).thenReturn("Access denied") + whenever(exception.statusCode).thenReturn(403) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.getFactors()).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument, MyAccountException>>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("access_denied"), eq("Access denied"), any()) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt new file mode 100644 index 000000000..c6ab8f515 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt @@ -0,0 +1,142 @@ +package com.auth0.auth0_flutter.request_handlers.my_account + +import com.auth0.android.Auth0 +import com.auth0.android.callback.Callback +import com.auth0.android.myaccount.MyAccountAPIClient +import com.auth0.android.myaccount.MyAccountException +import com.auth0.android.request.Request +import com.auth0.android.result.AuthenticationMethod +import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import io.flutter.plugin.common.MethodChannel.Result +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.* +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class VerifyOtpRequestHandlerTest { + + @Test + fun `should call verifyOtp with correct parameters`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "id" to "phone|test123", + "authSession" to "session456", + "otp" to "123456" + ) + val request = MethodCallRequest(account = mockAccount, options) + + whenever(mockClient.verifyOtp(any(), any(), any())).thenReturn(mockRequest) + + handler.handle(mockClient, request, mockResult) + + verify(mockClient).verifyOtp(eq("phone|test123"), eq("123456"), eq("session456")) + } + + @Test + fun `should call result success with null on success`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "id" to "phone|test123", + "authSession" to "session456", + "otp" to "123456" + ) + val request = MethodCallRequest(account = mockAccount, options) + + val mockMethod = mock() + + whenever(mockClient.verifyOtp(any(), any(), any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onSuccess(mockMethod) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).success(isNull()) + } + + @Test + fun `should call result error on failure`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val mockRequest = mock>() + val options = hashMapOf( + "id" to "phone|test123", + "authSession" to "session456", + "otp" to "wrong" + ) + val request = MethodCallRequest(account = mockAccount, options) + val exception = mock() + + whenever(exception.getCode()).thenReturn("invalid_code") + whenever(exception.getDescription()).thenReturn("Invalid code") + whenever(exception.statusCode).thenReturn(403) + whenever(exception.isNetworkError).thenReturn(false) + + whenever(mockClient.verifyOtp(any(), any(), any())).thenReturn(mockRequest) + doAnswer { + val callback = it.getArgument>(0) + callback.onFailure(exception) + }.whenever(mockRequest).start(any()) + + handler.handle(mockClient, request, mockResult) + + verify(mockResult).error(eq("invalid_code"), eq("Invalid code"), any()) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when id is missing`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf( + "authSession" to "session456", + "otp" to "123456" + ) + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when authSession is missing`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf( + "id" to "phone|test123", + "otp" to "123456" + ) + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } + + @Test(expected = IllegalArgumentException::class) + fun `should throw when otp is missing`() { + val handler = VerifyOtpRequestHandler() + val mockResult = mock() + val mockAccount = mock() + val mockClient = mock() + val options = hashMapOf( + "id" to "phone|test123", + "authSession" to "session456" + ) + val request = MethodCallRequest(account = mockAccount, options) + + handler.handle(mockClient, request, mockResult) + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift new file mode 100644 index 000000000..b00656d0e --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift @@ -0,0 +1,29 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountDeleteAuthMethodMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let id = arguments["id"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("id"))) + } + + client + .authenticationMethods + .deleteAuthenticationMethod(by: id) + .start { + switch $0 { + case .success: + callback(nil) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift new file mode 100644 index 000000000..54768136f --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift @@ -0,0 +1,29 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountEnrollEmailMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let email = arguments["email"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("email"))) + } + + client + .authenticationMethods + .enrollEmail(emailAddress: email) + .start { + switch $0 { + case let .success(challenge): + callback(challenge.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift new file mode 100644 index 000000000..815382645 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift @@ -0,0 +1,34 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountEnrollPhoneMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let phoneNumber = arguments["phoneNumber"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("phoneNumber"))) + } + guard let typeString = arguments["type"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("type"))) + } + + let preferredMethod: PreferredAuthenticationMethod = typeString == "voice" ? .voice : .sms + + client + .authenticationMethods + .enrollPhone(phoneNumber: phoneNumber, preferredAuthenticationMethod: preferredMethod) + .start { + switch $0 { + case let .success(challenge): + callback(challenge.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift new file mode 100644 index 000000000..0318e7814 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift @@ -0,0 +1,25 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountEnrollPushMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + client + .authenticationMethods + .enrollPushNotification() + .start { + switch $0 { + case let .success(challenge): + callback(challenge.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift new file mode 100644 index 000000000..0bbaefe50 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift @@ -0,0 +1,25 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountEnrollRecoveryCodeMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + client + .authenticationMethods + .enrollRecoveryCode() + .start { + switch $0 { + case let .success(challenge): + callback(challenge.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift new file mode 100644 index 000000000..307a3b31c --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift @@ -0,0 +1,25 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountEnrollTotpMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + client + .authenticationMethods + .enrollTOTP() + .start { + switch $0 { + case let .success(challenge): + callback(challenge.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift new file mode 100644 index 000000000..645e31e18 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift @@ -0,0 +1,103 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +extension FlutterError { + convenience init(from error: MyAccountError) { + let details: [String: Any] = [ + "_statusCode": error.statusCode, + "_errorFlags": [ + "isNetworkError": false + ] + ] + self.init(code: error.code, + message: error.detail.isEmpty ? error.title : error.detail, + details: details) + } +} + +extension AuthenticationMethod { + func asDictionary() -> [String: Any?] { + return [ + "id": id, + "type": type, + "name": name, + "phone_number": phoneNumber, + "email": nil, + "totp_secret": nil, + "totp_uri": nil, + "preferred_authentication_method": preferredAuthenticationMethod, + "created_at": createdAt, + "last_auth_at": nil + ] + } +} + +extension PhoneEnrollmentChallenge { + func asDictionary() -> [String: Any?] { + return [ + "id": authenticationId, + "auth_session": authenticationSession, + "totp_secret": nil, + "totp_uri": nil, + "barcode_uri": nil, + "recovery_code": nil + ] + } +} + +extension EmailEnrollmentChallenge { + func asDictionary() -> [String: Any?] { + return [ + "id": authenticationId, + "auth_session": authenticationSession, + "totp_secret": nil, + "totp_uri": nil, + "barcode_uri": nil, + "recovery_code": nil + ] + } +} + +extension TOTPEnrollmentChallenge { + func asDictionary() -> [String: Any?] { + return [ + "id": authenticationId, + "auth_session": authenticationSession, + "totp_secret": authenticatorManualInputCode, + "totp_uri": authenticatorQRCodeURI, + "barcode_uri": authenticatorQRCodeURI, + "recovery_code": nil + ] + } +} + +extension PushEnrollmentChallenge { + func asDictionary() -> [String: Any?] { + return [ + "id": authenticationId, + "auth_session": authenticationSession, + "totp_secret": nil, + "totp_uri": nil, + "barcode_uri": authenticatorQRCodeURI, + "recovery_code": nil + ] + } +} + +extension RecoveryCodeEnrollmentChallenge { + func asDictionary() -> [String: Any?] { + return [ + "id": authenticationId, + "auth_session": authenticationSession, + "totp_secret": nil, + "totp_uri": nil, + "barcode_uri": nil, + "recovery_code": recoveryCode + ] + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift new file mode 100644 index 000000000..ac819216f --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift @@ -0,0 +1,29 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountGetAuthMethodMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let id = arguments["id"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("id"))) + } + + client + .authenticationMethods + .getAuthenticationMethod(by: id) + .start { + switch $0 { + case let .success(method): + callback(method.asDictionary()) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift new file mode 100644 index 000000000..6b77761b1 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift @@ -0,0 +1,25 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountGetAuthMethodsMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + client + .authenticationMethods + .getAuthenticationMethods() + .start { + switch $0 { + case let .success(methods): + callback(methods.map { $0.asDictionary() }) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift new file mode 100644 index 000000000..386ee914b --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift @@ -0,0 +1,25 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountGetFactorsMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + client + .authenticationMethods + .getFactors() + .start { + switch $0 { + case let .success(factors): + callback(factors.map { ["name": $0.type, "enabled": true] }) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountHandler.swift new file mode 100644 index 000000000..4937adf6e --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountHandler.swift @@ -0,0 +1,86 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +typealias MyAccountClientProvider = (_ account: Account, _ userAgent: UserAgent, _ accessToken: String) -> MyAccount +typealias MyAccountMethodHandlerProvider = (_ method: MyAccountHandler.Method, _ client: MyAccount) -> MethodHandler + +public class MyAccountHandler: NSObject, FlutterPlugin { + enum Method: String, CaseIterable { + case getAuthenticationMethods = "myAccount#getAuthenticationMethods" + case getAuthenticationMethod = "myAccount#getAuthenticationMethod" + case deleteAuthenticationMethod = "myAccount#deleteAuthenticationMethod" + case getFactors = "myAccount#getFactors" + case enrollPhone = "myAccount#enrollPhone" + case enrollEmail = "myAccount#enrollEmail" + case enrollTotp = "myAccount#enrollTotp" + case enrollPush = "myAccount#enrollPush" + case enrollRecoveryCode = "myAccount#enrollRecoveryCode" + case verifyOtp = "myAccount#verifyOtp" + } + + private static let channelName = "auth0.com/auth0_flutter/my_account" + + public static func register(with registrar: FlutterPluginRegistrar) { + let handler = MyAccountHandler() + + #if os(iOS) + let channel = FlutterMethodChannel(name: MyAccountHandler.channelName, + binaryMessenger: registrar.messenger()) + #else + let channel = FlutterMethodChannel(name: MyAccountHandler.channelName, + binaryMessenger: registrar.messenger) + #endif + + registrar.addMethodCallDelegate(handler, channel: channel) + } + + var clientProvider: MyAccountClientProvider = { account, userAgent, accessToken in + var client = Auth0.myAccount(token: accessToken, domain: account.domain) + return client + } + + var methodHandlerProvider: MyAccountMethodHandlerProvider = { method, client in + switch method { + case .getAuthenticationMethods: return MyAccountGetAuthMethodsMethodHandler(client: client) + case .getAuthenticationMethod: return MyAccountGetAuthMethodMethodHandler(client: client) + case .deleteAuthenticationMethod: return MyAccountDeleteAuthMethodMethodHandler(client: client) + case .getFactors: return MyAccountGetFactorsMethodHandler(client: client) + case .enrollPhone: return MyAccountEnrollPhoneMethodHandler(client: client) + case .enrollEmail: return MyAccountEnrollEmailMethodHandler(client: client) + case .enrollTotp: return MyAccountEnrollTotpMethodHandler(client: client) + case .enrollPush: return MyAccountEnrollPushMethodHandler(client: client) + case .enrollRecoveryCode: return MyAccountEnrollRecoveryCodeMethodHandler(client: client) + case .verifyOtp: return MyAccountVerifyOtpMethodHandler(client: client) + } + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let arguments = call.arguments as? [String: Any] else { + return result(FlutterError(from: .argumentsMissing)) + } + guard let accountDictionary = arguments[Account.key] as? [String: String], + let account = Account(from: accountDictionary) else { + return result(FlutterError(from: .accountMissing)) + } + guard let userAgentDictionary = arguments[UserAgent.key] as? [String: String], + let userAgent = UserAgent(from: userAgentDictionary) else { + return result(FlutterError(from: .userAgentMissing)) + } + guard let method = Method(rawValue: call.method) else { + return result(FlutterMethodNotImplemented) + } + guard let accessToken = arguments["accessToken"] as? String else { + return result(FlutterError(from: .requiredArgumentMissing("accessToken"))) + } + + let client = clientProvider(account, userAgent, accessToken) + let methodHandler = methodHandlerProvider(method, client) + + methodHandler.handle(with: arguments, callback: result) + } +} diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift new file mode 100644 index 000000000..8c2c423d3 --- /dev/null +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift @@ -0,0 +1,35 @@ +import Auth0 + +#if os(iOS) +import Flutter +#else +import FlutterMacOS +#endif + +struct MyAccountVerifyOtpMethodHandler: MethodHandler { + let client: MyAccount + + func handle(with arguments: [String: Any], callback: @escaping FlutterResult) { + guard let id = arguments["id"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("id"))) + } + guard let authSession = arguments["authSession"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("authSession"))) + } + guard let otp = arguments["otp"] as? String else { + return callback(FlutterError(from: .requiredArgumentMissing("otp"))) + } + + client + .authenticationMethods + .confirmPhoneEnrollment(id: id, authSession: authSession, otpCode: otp) + .start { + switch $0 { + case .success: + callback(nil) + case let .failure(error): + callback(FlutterError(from: error)) + } + } + } +} diff --git a/auth0_flutter/darwin/Classes/SwiftAuth0FlutterPlugin.swift b/auth0_flutter/darwin/Classes/SwiftAuth0FlutterPlugin.swift index 490436710..74b9b37a2 100644 --- a/auth0_flutter/darwin/Classes/SwiftAuth0FlutterPlugin.swift +++ b/auth0_flutter/darwin/Classes/SwiftAuth0FlutterPlugin.swift @@ -8,7 +8,8 @@ public class SwiftAuth0FlutterPlugin: NSObject, FlutterPlugin { static var handlers: [FlutterPlugin.Type] = [WebAuthHandler.self, AuthAPIHandler.self, DPoPHandler.self, - CredentialsManagerHandler.self] + CredentialsManagerHandler.self, + MyAccountHandler.self] public static func register(with registrar: FlutterPluginRegistrar) { handlers.forEach { $0.register(with: registrar) } diff --git a/auth0_flutter/lib/auth0_flutter.dart b/auth0_flutter/lib/auth0_flutter.dart index 3e46ade85..79292699e 100644 --- a/auth0_flutter/lib/auth0_flutter.dart +++ b/auth0_flutter/lib/auth0_flutter.dart @@ -2,28 +2,35 @@ import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interfac import 'src/mobile/authentication_api.dart'; import 'src/mobile/credentials_manager.dart'; +import 'src/mobile/my_account_api.dart'; import 'src/mobile/web_authentication.dart'; import 'src/version.dart'; export 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart' show - WebAuthenticationException, + AuthenticationMethod, ApiException, + ChallengeType, + Credentials, + CredentialsManagerException, + DatabaseUser, + EnrollmentChallenge, + Factor, IdTokenValidationConfig, + LocalAuthentication, + LocalAuthenticationLevel, + MyAccountException, + PasswordlessType, + PhoneType, SafariViewController, SafariViewControllerPresentationStyle, - Credentials, SSOCredentials, UserProfile, - DatabaseUser, - ChallengeType, - CredentialsManagerException, - PasswordlessType, - LocalAuthentication, - LocalAuthenticationLevel; + WebAuthenticationException; export 'src/mobile/authentication_api.dart'; export 'src/mobile/credentials_manager.dart'; +export 'src/mobile/my_account_api.dart'; export 'src/mobile/web_authentication.dart'; /// Primary interface for interacting with Auth0 using web authentication, @@ -104,6 +111,26 @@ class Auth0 { WebAuthentication(_account, _userAgent, scheme, useCredentialsManager ? credentialsManager : null); + /// Creates an instance of [MyAccountApi] for managing the user's + /// authentication methods (MFA factors) via the + /// [My Account API](https://auth0.com/docs/manage-users/my-account-api). + /// + /// Requires an [accessToken] with the appropriate `me` scopes and + /// audience `https://{domain}/me/`. + /// + /// **Note:** Only supported on mobile platforms (Android/iOS). Web is not + /// supported. + /// + /// Usage example: + /// + /// ```dart + /// final auth0 = Auth0('DOMAIN', 'CLIENT_ID'); + /// final myAccount = auth0.myAccount(accessToken: 'ACCESS_TOKEN'); + /// final methods = await myAccount.getAuthenticationMethods(); + /// ``` + MyAccountApi myAccount({required final String accessToken}) => + MyAccountApi(_account, _userAgent, accessToken); + /// Generates DPoP (Demonstrating Proof-of-Possession) headers for making /// authenticated API calls with DPoP-bound tokens. /// diff --git a/auth0_flutter/lib/src/mobile/my_account_api.dart b/auth0_flutter/lib/src/mobile/my_account_api.dart new file mode 100644 index 000000000..c4c519ea2 --- /dev/null +++ b/auth0_flutter/lib/src/mobile/my_account_api.dart @@ -0,0 +1,230 @@ +import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; + +/// An interface for interacting with the +/// [Auth0 My Account API](https://auth0.com/docs/manage-users/my-account-api). +/// +/// This class enables end-users to self-manage their authentication methods +/// (MFA factors) without requiring Management API tokens. +/// +/// It is not intended for you to instantiate this class yourself, as an +/// instance of it is available via `Auth0.myAccount(accessToken:)`. +/// +/// **Note:** This API is only supported on mobile platforms (Android/iOS). +/// Web is not supported. +/// +/// Usage example: +/// +/// ```dart +/// final auth0 = Auth0('DOMAIN', 'CLIENT_ID'); +/// final myAccount = auth0.myAccount(accessToken: 'ACCESS_TOKEN'); +/// +/// final methods = await myAccount.getAuthenticationMethods(); +/// ``` +class MyAccountApi { + final Account _account; + final UserAgent _userAgent; + final String _accessToken; + + MyAccountApi(this._account, this._userAgent, this._accessToken); + + /// Lists all authentication methods enrolled by the user. + /// + /// Requires an access token with the `read:me:authentication_methods` scope + /// and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final methods = await myAccount.getAuthenticationMethods(); + /// ``` + Future> getAuthenticationMethods() => + Auth0FlutterMyAccountPlatform.instance.getAuthenticationMethods( + _createApiRequest(MyAccountGetAuthMethodsOptions( + accessToken: _accessToken))); + + /// Gets a specific authentication method by [id]. + /// + /// Requires an access token with the `read:me:authentication_methods` scope + /// and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final method = await myAccount.getAuthenticationMethod( + /// id: 'auth_method_id', + /// ); + /// ``` + Future getAuthenticationMethod( + {required final String id}) => + Auth0FlutterMyAccountPlatform.instance.getAuthenticationMethod( + _createApiRequest(MyAccountGetAuthMethodOptions( + accessToken: _accessToken, id: id))); + + /// Deletes an authentication method by [id]. + /// + /// Requires an access token with the `delete:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// await myAccount.deleteAuthenticationMethod(id: 'auth_method_id'); + /// ``` + Future deleteAuthenticationMethod({required final String id}) => + Auth0FlutterMyAccountPlatform.instance.deleteAuthenticationMethod( + _createApiRequest(MyAccountDeleteAuthMethodOptions( + accessToken: _accessToken, id: id))); + + /// Lists the factors available for enrollment on the tenant. + /// + /// Requires an access token with the `read:me:factors` scope + /// and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final factors = await myAccount.getFactors(); + /// ``` + Future> getFactors() => + Auth0FlutterMyAccountPlatform.instance.getFactors(_createApiRequest( + MyAccountGetFactorsOptions(accessToken: _accessToken))); + + /// Initiates phone enrollment with the specified [phoneNumber] and [type]. + /// + /// Returns an [EnrollmentChallenge] containing the enrollment `id` and + /// `authSession` needed to complete verification via [verifyOtp]. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final challenge = await myAccount.enrollPhone( + /// phoneNumber: '+1234567890', + /// type: PhoneType.sms, + /// ); + /// ``` + Future enrollPhone({ + required final String phoneNumber, + required final PhoneType type, + }) => + Auth0FlutterMyAccountPlatform.instance.enrollPhone(_createApiRequest( + MyAccountEnrollPhoneOptions( + accessToken: _accessToken, + phoneNumber: phoneNumber, + type: type))); + + /// Initiates email enrollment with the specified [email] address. + /// + /// Returns an [EnrollmentChallenge] containing the enrollment `id` and + /// `authSession` needed to complete verification via [verifyOtp]. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final challenge = await myAccount.enrollEmail( + /// email: 'user@example.com', + /// ); + /// ``` + Future enrollEmail({required final String email}) => + Auth0FlutterMyAccountPlatform.instance.enrollEmail(_createApiRequest( + MyAccountEnrollEmailOptions( + accessToken: _accessToken, email: email))); + + /// Initiates TOTP (authenticator app) enrollment. + /// + /// Returns an [EnrollmentChallenge] containing the `totpSecret` and + /// `totpUri` for the authenticator app, along with `id` and `authSession` + /// needed to complete verification via [verifyOtp]. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final challenge = await myAccount.enrollTotp(); + /// // Use challenge.totpUri to display a QR code + /// ``` + Future enrollTotp() => + Auth0FlutterMyAccountPlatform.instance.enrollTotp( + _createApiRequest(MyAccountEnrollTotpOptions( + accessToken: _accessToken))); + + /// Initiates push notification enrollment. + /// + /// Returns an [EnrollmentChallenge] containing the enrollment `id` and + /// `authSession` needed to complete the enrollment. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final challenge = await myAccount.enrollPush(); + /// ``` + Future enrollPush() => + Auth0FlutterMyAccountPlatform.instance.enrollPush( + _createApiRequest(MyAccountEnrollPushOptions( + accessToken: _accessToken))); + + /// Initiates recovery code enrollment. + /// + /// Returns an [EnrollmentChallenge] containing the `recoveryCode`. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// final challenge = await myAccount.enrollRecoveryCode(); + /// // Store challenge.recoveryCode securely + /// ``` + Future enrollRecoveryCode() => + Auth0FlutterMyAccountPlatform.instance.enrollRecoveryCode( + _createApiRequest(MyAccountEnrollRecoveryCodeOptions( + accessToken: _accessToken))); + + /// Verifies an enrollment using a one-time password [otp]. + /// + /// Use this after calling an enrollment method (e.g., [enrollPhone], + /// [enrollEmail], [enrollTotp]) to confirm the enrollment. + /// + /// [id] and [authSession] are obtained from the [EnrollmentChallenge] + /// returned by the enrollment method. + /// + /// Requires an access token with the `create:me:authentication_methods` + /// scope and audience `https://{domain}/me/`. + /// + /// ## Usage example + /// + /// ```dart + /// await myAccount.verifyOtp( + /// id: challenge.id, + /// authSession: challenge.authSession, + /// otp: '123456', + /// ); + /// ``` + Future verifyOtp({ + required final String id, + required final String authSession, + required final String otp, + }) => + Auth0FlutterMyAccountPlatform.instance.verifyOtp(_createApiRequest( + MyAccountVerifyOtpOptions( + accessToken: _accessToken, + id: id, + authSession: authSession, + otp: otp))); + + ApiRequest _createApiRequest( + final TOptions options) => + ApiRequest( + account: _account, options: options, userAgent: _userAgent); +} diff --git a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart index 53a0f3328..1b12c59f1 100644 --- a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart +++ b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart @@ -57,3 +57,20 @@ export 'src/web/exchange_token_options.dart'; export 'src/web/logout_options.dart'; export 'src/web/popup_login_options.dart'; export 'src/web/web_exception.dart'; +export 'src/myaccount/authentication_method.dart'; +export 'src/myaccount/enrollment_challenge.dart'; +export 'src/myaccount/factor.dart'; +export 'src/myaccount/my_account_exception.dart'; +export 'src/myaccount/phone_type.dart'; +export 'src/myaccount/auth0_flutter_my_account_platform.dart'; +export 'src/myaccount/method_channel_auth0_flutter_my_account.dart'; +export 'src/myaccount/my_account_get_auth_methods_options.dart'; +export 'src/myaccount/my_account_get_auth_method_options.dart'; +export 'src/myaccount/my_account_delete_auth_method_options.dart'; +export 'src/myaccount/my_account_get_factors_options.dart'; +export 'src/myaccount/my_account_enroll_phone_options.dart'; +export 'src/myaccount/my_account_enroll_email_options.dart'; +export 'src/myaccount/my_account_enroll_totp_options.dart'; +export 'src/myaccount/my_account_enroll_push_options.dart'; +export 'src/myaccount/my_account_enroll_recovery_code_options.dart'; +export 'src/myaccount/my_account_verify_otp_options.dart'; diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart b/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart new file mode 100644 index 000000000..c1667175e --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart @@ -0,0 +1,84 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../request/request.dart'; +import 'authentication_method.dart'; +import 'enrollment_challenge.dart'; +import 'factor.dart'; +import 'method_channel_auth0_flutter_my_account.dart'; +import 'my_account_delete_auth_method_options.dart'; +import 'my_account_enroll_email_options.dart'; +import 'my_account_enroll_phone_options.dart'; +import 'my_account_enroll_push_options.dart'; +import 'my_account_enroll_recovery_code_options.dart'; +import 'my_account_enroll_totp_options.dart'; +import 'my_account_get_auth_method_options.dart'; +import 'my_account_get_auth_methods_options.dart'; +import 'my_account_get_factors_options.dart'; +import 'my_account_verify_otp_options.dart'; + +abstract class Auth0FlutterMyAccountPlatform extends PlatformInterface { + Auth0FlutterMyAccountPlatform() : super(token: _token); + + static Auth0FlutterMyAccountPlatform get instance => _instance; + static final Object _token = Object(); + static Auth0FlutterMyAccountPlatform _instance = + MethodChannelAuth0FlutterMyAccount(); + + static set instance(final Auth0FlutterMyAccountPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future> getAuthenticationMethods( + final ApiRequest request) { + throw UnimplementedError( + 'getAuthenticationMethods() has not been implemented'); + } + + Future getAuthenticationMethod( + final ApiRequest request) { + throw UnimplementedError( + 'getAuthenticationMethod() has not been implemented'); + } + + Future deleteAuthenticationMethod( + final ApiRequest request) { + throw UnimplementedError( + 'deleteAuthenticationMethod() has not been implemented'); + } + + Future> getFactors( + final ApiRequest request) { + throw UnimplementedError('getFactors() has not been implemented'); + } + + Future enrollPhone( + final ApiRequest request) { + throw UnimplementedError('enrollPhone() has not been implemented'); + } + + Future enrollEmail( + final ApiRequest request) { + throw UnimplementedError('enrollEmail() has not been implemented'); + } + + Future enrollTotp( + final ApiRequest request) { + throw UnimplementedError('enrollTotp() has not been implemented'); + } + + Future enrollPush( + final ApiRequest request) { + throw UnimplementedError('enrollPush() has not been implemented'); + } + + Future enrollRecoveryCode( + final ApiRequest request) { + throw UnimplementedError('enrollRecoveryCode() has not been implemented'); + } + + Future verifyOtp( + final ApiRequest request) { + throw UnimplementedError('verifyOtp() has not been implemented'); + } +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart b/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart new file mode 100644 index 000000000..38bd972c5 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart @@ -0,0 +1,57 @@ +class AuthenticationMethod { + final String id; + final String type; + final String? name; + final String? phoneNumber; + final String? email; + final String? totpSecret; + final String? totpUri; + final String? preferredAuthenticationMethod; + final DateTime? createdAt; + final DateTime? lastAuthAt; + + const AuthenticationMethod({ + required this.id, + required this.type, + this.name, + this.phoneNumber, + this.email, + this.totpSecret, + this.totpUri, + this.preferredAuthenticationMethod, + this.createdAt, + this.lastAuthAt, + }); + + factory AuthenticationMethod.fromMap(final Map result) => + AuthenticationMethod( + id: result['id'] as String, + type: result['type'] as String, + name: result['name'] as String?, + phoneNumber: result['phone_number'] as String?, + email: result['email'] as String?, + totpSecret: result['totp_secret'] as String?, + totpUri: result['totp_uri'] as String?, + preferredAuthenticationMethod: + result['preferred_authentication_method'] as String?, + createdAt: result['created_at'] != null + ? DateTime.parse(result['created_at'] as String) + : null, + lastAuthAt: result['last_auth_at'] != null + ? DateTime.parse(result['last_auth_at'] as String) + : null, + ); + + Map toMap() => { + 'id': id, + 'type': type, + 'name': name, + 'phone_number': phoneNumber, + 'email': email, + 'totp_secret': totpSecret, + 'totp_uri': totpUri, + 'preferred_authentication_method': preferredAuthenticationMethod, + 'created_at': createdAt?.toUtc().toIso8601String(), + 'last_auth_at': lastAuthAt?.toUtc().toIso8601String(), + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/enrollment_challenge.dart b/auth0_flutter_platform_interface/lib/src/myaccount/enrollment_challenge.dart new file mode 100644 index 000000000..1e13237a2 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/enrollment_challenge.dart @@ -0,0 +1,36 @@ +class EnrollmentChallenge { + final String id; + final String authSession; + final String? totpSecret; + final String? totpUri; + final String? barcodeUri; + final String? recoveryCode; + + const EnrollmentChallenge({ + required this.id, + required this.authSession, + this.totpSecret, + this.totpUri, + this.barcodeUri, + this.recoveryCode, + }); + + factory EnrollmentChallenge.fromMap(final Map result) => + EnrollmentChallenge( + id: result['id'] as String, + authSession: result['auth_session'] as String, + totpSecret: result['totp_secret'] as String?, + totpUri: result['totp_uri'] as String?, + barcodeUri: result['barcode_uri'] as String?, + recoveryCode: result['recovery_code'] as String?, + ); + + Map toMap() => { + 'id': id, + 'auth_session': authSession, + 'totp_secret': totpSecret, + 'totp_uri': totpUri, + 'barcode_uri': barcodeUri, + 'recovery_code': recoveryCode, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart b/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart new file mode 100644 index 000000000..0ac3328e2 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart @@ -0,0 +1,19 @@ +class Factor { + final String name; + final bool enabled; + + const Factor({ + required this.name, + required this.enabled, + }); + + factory Factor.fromMap(final Map result) => Factor( + name: result['name'] as String, + enabled: result['enabled'] as bool, + ); + + Map toMap() => { + 'name': name, + 'enabled': enabled, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart b/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart new file mode 100644 index 000000000..e44388a64 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart @@ -0,0 +1,171 @@ +import 'package:flutter/services.dart'; + +import '../request/request.dart'; +import '../request/request_options.dart'; +import 'auth0_flutter_my_account_platform.dart'; +import 'authentication_method.dart'; +import 'enrollment_challenge.dart'; +import 'factor.dart'; +import 'my_account_delete_auth_method_options.dart'; +import 'my_account_enroll_email_options.dart'; +import 'my_account_enroll_phone_options.dart'; +import 'my_account_enroll_push_options.dart'; +import 'my_account_enroll_recovery_code_options.dart'; +import 'my_account_enroll_totp_options.dart'; +import 'my_account_exception.dart'; +import 'my_account_get_auth_method_options.dart'; +import 'my_account_get_auth_methods_options.dart'; +import 'my_account_get_factors_options.dart'; +import 'my_account_verify_otp_options.dart'; + +const MethodChannel _channel = + MethodChannel('auth0.com/auth0_flutter/my_account'); + +const String myAccountGetAuthMethodsMethod = + 'myAccount#getAuthenticationMethods'; +const String myAccountGetAuthMethodMethod = + 'myAccount#getAuthenticationMethod'; +const String myAccountDeleteAuthMethodMethod = + 'myAccount#deleteAuthenticationMethod'; +const String myAccountGetFactorsMethod = 'myAccount#getFactors'; +const String myAccountEnrollPhoneMethod = 'myAccount#enrollPhone'; +const String myAccountEnrollEmailMethod = 'myAccount#enrollEmail'; +const String myAccountEnrollTotpMethod = 'myAccount#enrollTotp'; +const String myAccountEnrollPushMethod = 'myAccount#enrollPush'; +const String myAccountEnrollRecoveryCodeMethod = 'myAccount#enrollRecoveryCode'; +const String myAccountVerifyOtpMethod = 'myAccount#verifyOtp'; + +class MethodChannelAuth0FlutterMyAccount + extends Auth0FlutterMyAccountPlatform { + @override + Future> getAuthenticationMethods( + final ApiRequest request) async { + final List result = await invokeListRequest( + method: myAccountGetAuthMethodsMethod, request: request); + + return result + .map((final item) => AuthenticationMethod.fromMap( + Map.from(item as Map))) + .toList(); + } + + @override + Future getAuthenticationMethod( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountGetAuthMethodMethod, request: request); + + return AuthenticationMethod.fromMap(result); + } + + @override + Future deleteAuthenticationMethod( + final ApiRequest request) async { + await invokeMapRequest( + method: myAccountDeleteAuthMethodMethod, + request: request, + throwOnNull: false); + } + + @override + Future> getFactors( + final ApiRequest request) async { + final List result = await invokeListRequest( + method: myAccountGetFactorsMethod, request: request); + + return result + .map((final item) => + Factor.fromMap(Map.from(item as Map))) + .toList(); + } + + @override + Future enrollPhone( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountEnrollPhoneMethod, request: request); + + return EnrollmentChallenge.fromMap(result); + } + + @override + Future enrollEmail( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountEnrollEmailMethod, request: request); + + return EnrollmentChallenge.fromMap(result); + } + + @override + Future enrollTotp( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountEnrollTotpMethod, request: request); + + return EnrollmentChallenge.fromMap(result); + } + + @override + Future enrollPush( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountEnrollPushMethod, request: request); + + return EnrollmentChallenge.fromMap(result); + } + + @override + Future enrollRecoveryCode( + final ApiRequest request) async { + final Map result = await invokeMapRequest( + method: myAccountEnrollRecoveryCodeMethod, request: request); + + return EnrollmentChallenge.fromMap(result); + } + + @override + Future verifyOtp( + final ApiRequest request) async { + await invokeMapRequest( + method: myAccountVerifyOtpMethod, request: request, throwOnNull: false); + } + + Future> + invokeMapRequest({ + required final String method, + required final ApiRequest request, + final bool? throwOnNull = true, + }) async { + final Map? result; + try { + result = await _channel.invokeMapMethod(method, request.toMap()); + } on PlatformException catch (e) { + throw MyAccountException.fromPlatformException(e); + } + + if (result == null && throwOnNull == true) { + throw const MyAccountException.unknown('Channel returned null.'); + } + + return result ?? {}; + } + + Future> invokeListRequest({ + required final String method, + required final ApiRequest request, + }) async { + final List? result; + try { + result = await _channel.invokeListMethod(method, request.toMap()); + } on PlatformException catch (e) { + throw MyAccountException.fromPlatformException(e); + } + + if (result == null) { + throw const MyAccountException.unknown('Channel returned null.'); + } + + return result; + } +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_delete_auth_method_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_delete_auth_method_options.dart new file mode 100644 index 000000000..80c1c4b72 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_delete_auth_method_options.dart @@ -0,0 +1,17 @@ +import '../request/request_options.dart'; + +class MyAccountDeleteAuthMethodOptions implements RequestOptions { + final String accessToken; + final String id; + + MyAccountDeleteAuthMethodOptions({ + required this.accessToken, + required this.id, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'id': id, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_email_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_email_options.dart new file mode 100644 index 000000000..3f492b088 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_email_options.dart @@ -0,0 +1,17 @@ +import '../request/request_options.dart'; + +class MyAccountEnrollEmailOptions implements RequestOptions { + final String accessToken; + final String email; + + MyAccountEnrollEmailOptions({ + required this.accessToken, + required this.email, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'email': email, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_phone_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_phone_options.dart new file mode 100644 index 000000000..cc6e27ffd --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_phone_options.dart @@ -0,0 +1,21 @@ +import '../request/request_options.dart'; +import 'phone_type.dart'; + +class MyAccountEnrollPhoneOptions implements RequestOptions { + final String accessToken; + final String phoneNumber; + final PhoneType type; + + MyAccountEnrollPhoneOptions({ + required this.accessToken, + required this.phoneNumber, + required this.type, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'phoneNumber': phoneNumber, + 'type': type.toValue(), + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_push_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_push_options.dart new file mode 100644 index 000000000..970089471 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_push_options.dart @@ -0,0 +1,12 @@ +import '../request/request_options.dart'; + +class MyAccountEnrollPushOptions implements RequestOptions { + final String accessToken; + + MyAccountEnrollPushOptions({required this.accessToken}); + + @override + Map toMap() => { + 'accessToken': accessToken, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_recovery_code_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_recovery_code_options.dart new file mode 100644 index 000000000..7da403df0 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_recovery_code_options.dart @@ -0,0 +1,12 @@ +import '../request/request_options.dart'; + +class MyAccountEnrollRecoveryCodeOptions implements RequestOptions { + final String accessToken; + + MyAccountEnrollRecoveryCodeOptions({required this.accessToken}); + + @override + Map toMap() => { + 'accessToken': accessToken, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_totp_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_totp_options.dart new file mode 100644 index 000000000..5b2c5239d --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_enroll_totp_options.dart @@ -0,0 +1,12 @@ +import '../request/request_options.dart'; + +class MyAccountEnrollTotpOptions implements RequestOptions { + final String accessToken; + + MyAccountEnrollTotpOptions({required this.accessToken}); + + @override + Map toMap() => { + 'accessToken': accessToken, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart new file mode 100644 index 000000000..66dac61a6 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; + +import '../auth0_exception.dart'; +import '../extensions/exception_extensions.dart'; +import '../extensions/map_extensions.dart'; + +class MyAccountException extends Auth0Exception { + static const _statusCodeKey = '_statusCode'; + static const _errorFlagsKey = '_errorFlags'; + + final int statusCode; + final Map _errorFlags; + + const MyAccountException(final String code, final String message, + final Map details, this._errorFlags, this.statusCode) + : super(code, message, details); + + const MyAccountException.unknown(final String message) + : _errorFlags = const {}, + statusCode = 0, + super.unknown(message); + + factory MyAccountException.fromPlatformException(final PlatformException e) { + final Map errorDetails = e.detailsMap; + final statusCode = errorDetails.getOrDefault(_statusCodeKey, 0); + final errorFlags = + errorDetails.getOrDefault(_errorFlagsKey, {}); + + errorDetails.remove(_statusCodeKey); + errorDetails.remove(_errorFlagsKey); + + return MyAccountException( + e.code, e.messageString, errorDetails, errorFlags, statusCode); + } + + bool get isNetworkError => _errorFlags.getBooleanOrFalse('isNetworkError'); + bool get isRetryable => isNetworkError; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_method_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_method_options.dart new file mode 100644 index 000000000..47b6af305 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_method_options.dart @@ -0,0 +1,17 @@ +import '../request/request_options.dart'; + +class MyAccountGetAuthMethodOptions implements RequestOptions { + final String accessToken; + final String id; + + MyAccountGetAuthMethodOptions({ + required this.accessToken, + required this.id, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'id': id, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_methods_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_methods_options.dart new file mode 100644 index 000000000..a9ab2f27c --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_auth_methods_options.dart @@ -0,0 +1,12 @@ +import '../request/request_options.dart'; + +class MyAccountGetAuthMethodsOptions implements RequestOptions { + final String accessToken; + + MyAccountGetAuthMethodsOptions({required this.accessToken}); + + @override + Map toMap() => { + 'accessToken': accessToken, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_factors_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_factors_options.dart new file mode 100644 index 000000000..167a395ef --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_get_factors_options.dart @@ -0,0 +1,12 @@ +import '../request/request_options.dart'; + +class MyAccountGetFactorsOptions implements RequestOptions { + final String accessToken; + + MyAccountGetFactorsOptions({required this.accessToken}); + + @override + Map toMap() => { + 'accessToken': accessToken, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_verify_otp_options.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_verify_otp_options.dart new file mode 100644 index 000000000..61f8272e8 --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_verify_otp_options.dart @@ -0,0 +1,23 @@ +import '../request/request_options.dart'; + +class MyAccountVerifyOtpOptions implements RequestOptions { + final String accessToken; + final String id; + final String authSession; + final String otp; + + MyAccountVerifyOtpOptions({ + required this.accessToken, + required this.id, + required this.authSession, + required this.otp, + }); + + @override + Map toMap() => { + 'accessToken': accessToken, + 'id': id, + 'authSession': authSession, + 'otp': otp, + }; +} diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/phone_type.dart b/auth0_flutter_platform_interface/lib/src/myaccount/phone_type.dart new file mode 100644 index 000000000..11f7d243f --- /dev/null +++ b/auth0_flutter_platform_interface/lib/src/myaccount/phone_type.dart @@ -0,0 +1,24 @@ +enum PhoneType { + sms, + voice; + + String toValue() { + switch (this) { + case PhoneType.sms: + return 'sms'; + case PhoneType.voice: + return 'voice'; + } + } + + static PhoneType fromValue(final String value) { + switch (value) { + case 'sms': + return PhoneType.sms; + case 'voice': + return PhoneType.voice; + default: + return PhoneType.sms; + } + } +} From f29c838042192d61e707c4139cd527b487bab76a Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Wed, 20 May 2026 12:45:27 +0530 Subject: [PATCH 2/5] fix: add iOS/macOS symlinks and sort export directives alphabetically --- ...AccountDeleteAuthMethodMethodHandler.swift | 1 + .../MyAccountEnrollEmailMethodHandler.swift | 1 + .../MyAccountEnrollPhoneMethodHandler.swift | 1 + .../MyAccountEnrollPushMethodHandler.swift | 1 + ...countEnrollRecoveryCodeMethodHandler.swift | 1 + .../MyAccountEnrollTotpMethodHandler.swift | 1 + .../MyAccountAPI/MyAccountExtensions.swift | 1 + .../MyAccountGetAuthMethodMethodHandler.swift | 1 + ...MyAccountGetAuthMethodsMethodHandler.swift | 1 + .../MyAccountGetFactorsMethodHandler.swift | 1 + .../MyAccountAPI/MyAccountHandler.swift | 1 + .../MyAccountVerifyOtpMethodHandler.swift | 1 + ...AccountDeleteAuthMethodMethodHandler.swift | 1 + .../MyAccountEnrollEmailMethodHandler.swift | 1 + .../MyAccountEnrollPhoneMethodHandler.swift | 1 + .../MyAccountEnrollPushMethodHandler.swift | 1 + ...countEnrollRecoveryCodeMethodHandler.swift | 1 + .../MyAccountEnrollTotpMethodHandler.swift | 1 + .../MyAccountAPI/MyAccountExtensions.swift | 1 + .../MyAccountGetAuthMethodMethodHandler.swift | 1 + ...MyAccountGetAuthMethodsMethodHandler.swift | 1 + .../MyAccountGetFactorsMethodHandler.swift | 1 + .../MyAccountAPI/MyAccountHandler.swift | 1 + .../MyAccountVerifyOtpMethodHandler.swift | 1 + .../lib/auth0_flutter_platform_interface.dart | 34 +++++++++---------- 25 files changed, 41 insertions(+), 17 deletions(-) create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountExtensions.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountHandler.swift create mode 120000 auth0_flutter/ios/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountExtensions.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountHandler.swift create mode 120000 auth0_flutter/macos/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift new file mode 120000 index 000000000..2e3e8b4e0 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift new file mode 120000 index 000000000..daf0f0fe6 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift new file mode 120000 index 000000000..d0505ff08 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift new file mode 120000 index 000000000..75c6bbb10 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift new file mode 120000 index 000000000..a0823fa59 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift new file mode 120000 index 000000000..4728fd483 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountExtensions.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountExtensions.swift new file mode 120000 index 000000000..69a6d0a97 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountExtensions.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountExtensions.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift new file mode 120000 index 000000000..e1881a28b --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift new file mode 120000 index 000000000..3038cdde9 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift new file mode 120000 index 000000000..57eb6990e --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountHandler.swift new file mode 120000 index 000000000..db33d385b --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountHandler.swift \ No newline at end of file diff --git a/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift new file mode 120000 index 000000000..25a45bc22 --- /dev/null +++ b/auth0_flutter/ios/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift new file mode 120000 index 000000000..2e3e8b4e0 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountDeleteAuthMethodMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift new file mode 120000 index 000000000..daf0f0fe6 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollEmailMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift new file mode 120000 index 000000000..d0505ff08 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollPhoneMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift new file mode 120000 index 000000000..75c6bbb10 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollPushMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift new file mode 120000 index 000000000..a0823fa59 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollRecoveryCodeMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift new file mode 120000 index 000000000..4728fd483 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountEnrollTotpMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountExtensions.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountExtensions.swift new file mode 120000 index 000000000..69a6d0a97 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountExtensions.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountExtensions.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift new file mode 120000 index 000000000..e1881a28b --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift new file mode 120000 index 000000000..3038cdde9 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetAuthMethodsMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift new file mode 120000 index 000000000..57eb6990e --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountHandler.swift new file mode 120000 index 000000000..db33d385b --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountHandler.swift \ No newline at end of file diff --git a/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift new file mode 120000 index 000000000..25a45bc22 --- /dev/null +++ b/auth0_flutter/macos/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift @@ -0,0 +1 @@ +../../../darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift \ No newline at end of file diff --git a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart index 1b12c59f1..b8a3f62d4 100644 --- a/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart +++ b/auth0_flutter_platform_interface/lib/auth0_flutter_platform_interface.dart @@ -39,6 +39,23 @@ export 'src/login_options.dart'; export 'src/method_channel_auth0_flutter_auth.dart'; export 'src/method_channel_auth0_flutter_dpop.dart'; export 'src/method_channel_auth0_flutter_web_auth.dart'; +export 'src/myaccount/auth0_flutter_my_account_platform.dart'; +export 'src/myaccount/authentication_method.dart'; +export 'src/myaccount/enrollment_challenge.dart'; +export 'src/myaccount/factor.dart'; +export 'src/myaccount/method_channel_auth0_flutter_my_account.dart'; +export 'src/myaccount/my_account_delete_auth_method_options.dart'; +export 'src/myaccount/my_account_enroll_email_options.dart'; +export 'src/myaccount/my_account_enroll_phone_options.dart'; +export 'src/myaccount/my_account_enroll_push_options.dart'; +export 'src/myaccount/my_account_enroll_recovery_code_options.dart'; +export 'src/myaccount/my_account_enroll_totp_options.dart'; +export 'src/myaccount/my_account_exception.dart'; +export 'src/myaccount/my_account_get_auth_method_options.dart'; +export 'src/myaccount/my_account_get_auth_methods_options.dart'; +export 'src/myaccount/my_account_get_factors_options.dart'; +export 'src/myaccount/my_account_verify_otp_options.dart'; +export 'src/myaccount/phone_type.dart'; export 'src/request/dpop_request.dart'; export 'src/request/request.dart'; export 'src/request/request_options.dart'; @@ -57,20 +74,3 @@ export 'src/web/exchange_token_options.dart'; export 'src/web/logout_options.dart'; export 'src/web/popup_login_options.dart'; export 'src/web/web_exception.dart'; -export 'src/myaccount/authentication_method.dart'; -export 'src/myaccount/enrollment_challenge.dart'; -export 'src/myaccount/factor.dart'; -export 'src/myaccount/my_account_exception.dart'; -export 'src/myaccount/phone_type.dart'; -export 'src/myaccount/auth0_flutter_my_account_platform.dart'; -export 'src/myaccount/method_channel_auth0_flutter_my_account.dart'; -export 'src/myaccount/my_account_get_auth_methods_options.dart'; -export 'src/myaccount/my_account_get_auth_method_options.dart'; -export 'src/myaccount/my_account_delete_auth_method_options.dart'; -export 'src/myaccount/my_account_get_factors_options.dart'; -export 'src/myaccount/my_account_enroll_phone_options.dart'; -export 'src/myaccount/my_account_enroll_email_options.dart'; -export 'src/myaccount/my_account_enroll_totp_options.dart'; -export 'src/myaccount/my_account_enroll_push_options.dart'; -export 'src/myaccount/my_account_enroll_recovery_code_options.dart'; -export 'src/myaccount/my_account_verify_otp_options.dart'; From 2e3e50cc870e10174e18f542de7240aae5ea3e35 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Wed, 20 May 2026 13:53:53 +0530 Subject: [PATCH 3/5] fix: add missing valuePublished(byPlugin:) to SpyPluginRegistrar for ios test failure --- auth0_flutter/example/ios/Tests/Mocks.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/auth0_flutter/example/ios/Tests/Mocks.swift b/auth0_flutter/example/ios/Tests/Mocks.swift index 46774a436..98c7ff565 100644 --- a/auth0_flutter/example/ios/Tests/Mocks.swift +++ b/auth0_flutter/example/ios/Tests/Mocks.swift @@ -136,6 +136,10 @@ class SpyPluginRegistrar: NSObject, FlutterPluginRegistrar { func publish(_ value: NSObject) {} + func valuePublished(byPlugin pluginKey: String) -> NSObject? { + return nil + } + func lookupKey(forAsset asset: String) -> String { return "" } From efc7a35d9402e7c67d05e19ced5278a365d22da9 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 25 May 2026 11:57:36 +0530 Subject: [PATCH 4/5] fix: use barcodeUri for totp_uri in Android TOTP enrollment challenge mapping Signed-off-by: utkrishtS --- .../main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt index 47661076e..119fbdec6 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt @@ -49,7 +49,7 @@ fun EnrollmentChallenge.toMyAccountChallengeMap(): Map { is TotpEnrollmentChallenge -> { put("barcode_uri", challenge.barcodeUri) put("totp_secret", challenge.manualInputCode) - put("totp_uri", challenge.manualInputCode) + put("totp_uri", challenge.barcodeUri) } is RecoveryCodeEnrollmentChallenge -> { put("recovery_code", challenge.recoveryCode) From 7722a356506720f782b414c4ca8cb22ce98dfcd3 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 25 May 2026 12:33:25 +0530 Subject: [PATCH 5/5] fix: address PR review feedback - correct Factor/AuthenticationMethod models and verifyOtp return type --- .../MyAccountExceptionExtensions.kt | 2 ++ .../auth0/auth0_flutter/MyAccountExtensions.kt | 5 ++++- .../my_account/GetFactorsRequestHandler.kt | 4 ++-- .../my_account/VerifyOtpRequestHandler.kt | 3 ++- .../my_account/GetFactorsRequestHandlerTest.kt | 5 +++-- .../my_account/VerifyOtpRequestHandlerTest.kt | 14 ++++++++++++-- .../MyAccountAPI/MyAccountExtensions.swift | 6 +++++- .../MyAccountGetFactorsMethodHandler.swift | 2 +- .../MyAccountVerifyOtpMethodHandler.swift | 4 ++-- .../MyAccountVerifyOtpMethodHandlerTests.swift | 10 +++++++--- auth0_flutter/example/lib/my_account_card.dart | 7 ++++--- .../lib/src/mobile/my_account_api.dart | 7 +++++-- .../auth0_flutter_my_account_platform.dart | 2 +- .../src/myaccount/authentication_method.dart | 10 ++++++++++ .../lib/src/myaccount/factor.dart | 18 ++++++++++-------- ...ethod_channel_auth0_flutter_my_account.dart | 8 +++++--- .../src/myaccount/my_account_exception.dart | 16 ++++++++++++++-- 17 files changed, 89 insertions(+), 34 deletions(-) diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt index 9409b178c..ce4040786 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExceptionExtensions.kt @@ -6,6 +6,8 @@ fun MyAccountException.toMyAccountMap(): Map { val exception = this return buildMap { put("_statusCode", exception.statusCode) + put("_title", exception.getCode()) + put("_detail", exception.getDescription()) put("_errorFlags", mapOf( "isNetworkError" to exception.isNetworkError, )) diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt index 119fbdec6..309fd8822 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/MyAccountExtensions.kt @@ -33,10 +33,13 @@ fun AuthenticationMethod.toMyAccountMethodMap(): Map { is PushNotificationAuthenticationMethod -> { put("name", method.name) } - else -> {} + else -> { + put("name", null) + } } if (this@toMyAccountMethodMap is MfaAuthenticationMethod) { put("confirmed", confirmed) + put("usage", usage) } } } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt index 6d336447f..9a7590253 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandler.kt @@ -33,8 +33,8 @@ class GetFactorsRequestHandler : MyAccountRequestHandler { override fun onSuccess(res: List) { result.success(res.map { mapOf( - "name" to it.type, - "enabled" to true + "type" to it.type, + "usage" to it.usage ) }) } diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt index 575240d6e..a230c5607 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandler.kt @@ -6,6 +6,7 @@ import com.auth0.android.myaccount.MyAccountException import com.auth0.android.result.AuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import com.auth0.auth0_flutter.toMyAccountMap +import com.auth0.auth0_flutter.toMyAccountMethodMap import com.auth0.auth0_flutter.utils.assertHasProperties import io.flutter.plugin.common.MethodChannel @@ -44,7 +45,7 @@ class VerifyOtpRequestHandler : MyAccountRequestHandler { override fun onSuccess( res: AuthenticationMethod ) { - result.success(null) + result.success(res.toMyAccountMethodMap()) } }) } diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt index 5c98ed09f..008871c98 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/GetFactorsRequestHandlerTest.kt @@ -45,6 +45,7 @@ class GetFactorsRequestHandlerTest { val mockFactor = mock() whenever(mockFactor.type).thenReturn("sms") + whenever(mockFactor.usage).thenReturn(listOf("secondary")) whenever(mockClient.getFactors()).thenReturn(mockRequest) doAnswer { @@ -59,8 +60,8 @@ class GetFactorsRequestHandlerTest { val result = captor.firstValue assertThat(result.size, equalTo(1)) - assertThat(result[0]["name"], equalTo("sms")) - assertThat(result[0]["enabled"], equalTo(true)) + assertThat(result[0]["type"], equalTo("sms")) + assertThat(result[0]["usage"], equalTo(listOf("secondary"))) } @Test diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt index c6ab8f515..91816abe0 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/request_handlers/my_account/VerifyOtpRequestHandlerTest.kt @@ -8,6 +8,8 @@ import com.auth0.android.request.Request import com.auth0.android.result.AuthenticationMethod import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import io.flutter.plugin.common.MethodChannel.Result +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.* @@ -38,7 +40,7 @@ class VerifyOtpRequestHandlerTest { } @Test - fun `should call result success with null on success`() { + fun `should call result success with mapped method on success`() { val handler = VerifyOtpRequestHandler() val mockResult = mock() val mockAccount = mock() @@ -52,6 +54,9 @@ class VerifyOtpRequestHandlerTest { val request = MethodCallRequest(account = mockAccount, options) val mockMethod = mock() + whenever(mockMethod.id).thenReturn("phone|test123") + whenever(mockMethod.type).thenReturn("phone") + whenever(mockMethod.createdAt).thenReturn("2026-01-01") whenever(mockClient.verifyOtp(any(), any(), any())).thenReturn(mockRequest) doAnswer { @@ -61,7 +66,12 @@ class VerifyOtpRequestHandlerTest { handler.handle(mockClient, request, mockResult) - verify(mockResult).success(isNull()) + val captor = argumentCaptor>() + verify(mockResult).success(captor.capture()) + + val result = captor.firstValue + assertThat(result["id"], equalTo("phone|test123")) + assertThat(result["type"], equalTo("phone")) } @Test diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift index 645e31e18..64b87f596 100644 --- a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountExtensions.swift @@ -10,6 +10,8 @@ extension FlutterError { convenience init(from error: MyAccountError) { let details: [String: Any] = [ "_statusCode": error.statusCode, + "_title": error.title, + "_detail": error.detail, "_errorFlags": [ "isNetworkError": false ] @@ -32,7 +34,9 @@ extension AuthenticationMethod { "totp_uri": nil, "preferred_authentication_method": preferredAuthenticationMethod, "created_at": createdAt, - "last_auth_at": nil + "last_auth_at": nil, + "confirmed": confirmed, + "usage": usage ] } } diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift index 386ee914b..8f35c6ef5 100644 --- a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountGetFactorsMethodHandler.swift @@ -16,7 +16,7 @@ struct MyAccountGetFactorsMethodHandler: MethodHandler { .start { switch $0 { case let .success(factors): - callback(factors.map { ["name": $0.type, "enabled": true] }) + callback(factors.map { ["type": $0.type, "usage": $0.usage as Any] }) case let .failure(error): callback(FlutterError(from: error)) } diff --git a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift index 8c2c423d3..c261c0534 100644 --- a/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift +++ b/auth0_flutter/darwin/Classes/MyAccountAPI/MyAccountVerifyOtpMethodHandler.swift @@ -25,8 +25,8 @@ struct MyAccountVerifyOtpMethodHandler: MethodHandler { .confirmPhoneEnrollment(id: id, authSession: authSession, otpCode: otp) .start { switch $0 { - case .success: - callback(nil) + case let .success(method): + callback(method.asDictionary()) case let .failure(error): callback(FlutterError(from: error)) } diff --git a/auth0_flutter/example/ios/Tests/MyAccount/MyAccountVerifyOtpMethodHandlerTests.swift b/auth0_flutter/example/ios/Tests/MyAccount/MyAccountVerifyOtpMethodHandlerTests.swift index dcdd38076..c4e4eb17b 100644 --- a/auth0_flutter/example/ios/Tests/MyAccount/MyAccountVerifyOtpMethodHandlerTests.swift +++ b/auth0_flutter/example/ios/Tests/MyAccount/MyAccountVerifyOtpMethodHandlerTests.swift @@ -51,10 +51,14 @@ class MyAccountVerifyOtpMethodHandlerTests: XCTestCase { wait(for: [expectation]) } - func testProducesNilOnSuccess() { - let expectation = self.expectation(description: "Produced nil") + func testProducesDictionaryOnSuccess() { + let expectation = self.expectation(description: "Produced dictionary") sut.handle(with: ["id": "test-id", "authSession": "session", "otp": "123456"]) { result in - XCTAssertNil(result) + guard let dict = result as? [String: Any?] else { + return XCTFail("Did not produce dictionary") + } + XCTAssertEqual(dict["id"] as? String, "test-id") + XCTAssertEqual(dict["confirmed"] as? Bool, true) expectation.fulfill() } wait(for: [expectation]) diff --git a/auth0_flutter/example/lib/my_account_card.dart b/auth0_flutter/example/lib/my_account_card.dart index 18aad751c..642cec7cb 100644 --- a/auth0_flutter/example/lib/my_account_card.dart +++ b/auth0_flutter/example/lib/my_account_card.dart @@ -130,7 +130,7 @@ class _MyAccountCardState extends State { } final output = StringBuffer('Available Factors (${factors.length}):\n'); for (final f in factors) { - output.writeln(' - ${f.name}: ${f.enabled ? "enabled" : "disabled"}'); + output.writeln(' - ${f.type}: usage=${f.usage?.join(", ") ?? "none"}'); } _log(output.toString()); } on MyAccountException catch (e) { @@ -286,12 +286,13 @@ class _MyAccountCardState extends State { ' id: $_lastEnrollmentId,\n' ' authSession: $_lastAuthSession,\n' ' otp: $otp)...'); - await _myAccount!.verifyOtp( + final method = await _myAccount!.verifyOtp( id: _lastEnrollmentId!, authSession: _lastAuthSession!, otp: otp, ); - _log('OTP Verified Successfully! Enrollment complete.'); + _log('OTP Verified Successfully! Enrollment complete.\n' + ' Method: ${method.type} (id: ${method.id})'); setState(() { _lastEnrollmentId = null; _lastAuthSession = null; diff --git a/auth0_flutter/lib/src/mobile/my_account_api.dart b/auth0_flutter/lib/src/mobile/my_account_api.dart index c4c519ea2..03d706046 100644 --- a/auth0_flutter/lib/src/mobile/my_account_api.dart +++ b/auth0_flutter/lib/src/mobile/my_account_api.dart @@ -196,6 +196,9 @@ class MyAccountApi { /// Use this after calling an enrollment method (e.g., [enrollPhone], /// [enrollEmail], [enrollTotp]) to confirm the enrollment. /// + /// Returns the confirmed [AuthenticationMethod] after successful + /// verification. + /// /// [id] and [authSession] are obtained from the [EnrollmentChallenge] /// returned by the enrollment method. /// @@ -205,13 +208,13 @@ class MyAccountApi { /// ## Usage example /// /// ```dart - /// await myAccount.verifyOtp( + /// final method = await myAccount.verifyOtp( /// id: challenge.id, /// authSession: challenge.authSession, /// otp: '123456', /// ); /// ``` - Future verifyOtp({ + Future verifyOtp({ required final String id, required final String authSession, required final String otp, diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart b/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart index c1667175e..93238f1d9 100644 --- a/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart +++ b/auth0_flutter_platform_interface/lib/src/myaccount/auth0_flutter_my_account_platform.dart @@ -77,7 +77,7 @@ abstract class Auth0FlutterMyAccountPlatform extends PlatformInterface { throw UnimplementedError('enrollRecoveryCode() has not been implemented'); } - Future verifyOtp( + Future verifyOtp( final ApiRequest request) { throw UnimplementedError('verifyOtp() has not been implemented'); } diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart b/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart index 38bd972c5..5d39b94f7 100644 --- a/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart +++ b/auth0_flutter_platform_interface/lib/src/myaccount/authentication_method.dart @@ -9,6 +9,8 @@ class AuthenticationMethod { final String? preferredAuthenticationMethod; final DateTime? createdAt; final DateTime? lastAuthAt; + final bool? confirmed; + final List? usage; const AuthenticationMethod({ required this.id, @@ -21,6 +23,8 @@ class AuthenticationMethod { this.preferredAuthenticationMethod, this.createdAt, this.lastAuthAt, + this.confirmed, + this.usage, }); factory AuthenticationMethod.fromMap(final Map result) => @@ -40,6 +44,10 @@ class AuthenticationMethod { lastAuthAt: result['last_auth_at'] != null ? DateTime.parse(result['last_auth_at'] as String) : null, + confirmed: result['confirmed'] as bool?, + usage: (result['usage'] as List?) + ?.map((final e) => e as String) + .toList(), ); Map toMap() => { @@ -53,5 +61,7 @@ class AuthenticationMethod { 'preferred_authentication_method': preferredAuthenticationMethod, 'created_at': createdAt?.toUtc().toIso8601String(), 'last_auth_at': lastAuthAt?.toUtc().toIso8601String(), + 'confirmed': confirmed, + 'usage': usage, }; } diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart b/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart index 0ac3328e2..cabf70255 100644 --- a/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart +++ b/auth0_flutter_platform_interface/lib/src/myaccount/factor.dart @@ -1,19 +1,21 @@ class Factor { - final String name; - final bool enabled; + final String type; + final List? usage; const Factor({ - required this.name, - required this.enabled, + required this.type, + this.usage, }); factory Factor.fromMap(final Map result) => Factor( - name: result['name'] as String, - enabled: result['enabled'] as bool, + type: result['type'] as String, + usage: (result['usage'] as List?) + ?.map((final e) => e as String) + .toList(), ); Map toMap() => { - 'name': name, - 'enabled': enabled, + 'type': type, + 'usage': usage, }; } diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart b/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart index e44388a64..9855fab2f 100644 --- a/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart +++ b/auth0_flutter_platform_interface/lib/src/myaccount/method_channel_auth0_flutter_my_account.dart @@ -125,10 +125,12 @@ class MethodChannelAuth0FlutterMyAccount } @override - Future verifyOtp( + Future verifyOtp( final ApiRequest request) async { - await invokeMapRequest( - method: myAccountVerifyOtpMethod, request: request, throwOnNull: false); + final Map result = await invokeMapRequest( + method: myAccountVerifyOtpMethod, request: request); + + return AuthenticationMethod.fromMap(result); } Future> diff --git a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart index 66dac61a6..756114596 100644 --- a/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart +++ b/auth0_flutter_platform_interface/lib/src/myaccount/my_account_exception.dart @@ -6,31 +6,43 @@ import '../extensions/map_extensions.dart'; class MyAccountException extends Auth0Exception { static const _statusCodeKey = '_statusCode'; + static const _titleKey = '_title'; + static const _detailKey = '_detail'; static const _errorFlagsKey = '_errorFlags'; final int statusCode; + final String title; + final String detail; final Map _errorFlags; const MyAccountException(final String code, final String message, - final Map details, this._errorFlags, this.statusCode) + final Map details, this._errorFlags, this.statusCode, + this.title, this.detail) : super(code, message, details); const MyAccountException.unknown(final String message) : _errorFlags = const {}, statusCode = 0, + title = '', + detail = '', super.unknown(message); factory MyAccountException.fromPlatformException(final PlatformException e) { final Map errorDetails = e.detailsMap; final statusCode = errorDetails.getOrDefault(_statusCodeKey, 0); + final title = errorDetails.getOrDefault(_titleKey, ''); + final detail = errorDetails.getOrDefault(_detailKey, ''); final errorFlags = errorDetails.getOrDefault(_errorFlagsKey, {}); errorDetails.remove(_statusCodeKey); + errorDetails.remove(_titleKey); + errorDetails.remove(_detailKey); errorDetails.remove(_errorFlagsKey); return MyAccountException( - e.code, e.messageString, errorDetails, errorFlags, statusCode); + e.code, e.messageString, errorDetails, errorFlags, statusCode, + title, detail); } bool get isNetworkError => _errorFlags.getBooleanOrFalse('isNetworkError');