diff --git a/packages/amplify_auth_cognito/android/build.gradle b/packages/amplify_auth_cognito/android/build.gradle index a124060127..69c329bbf7 100644 --- a/packages/amplify_auth_cognito/android/build.gradle +++ b/packages/amplify_auth_cognito/android/build.gradle @@ -49,6 +49,7 @@ android { testOptions { unitTests { includeAndroidResources = true + returnDefaultValues = true } } buildTypes { @@ -63,8 +64,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.amplifyframework:aws-auth-cognito:1.22.0' testImplementation 'junit:junit:4.13' - testImplementation 'org.mockito:mockito-core:3.1.0' + testImplementation 'org.mockito:mockito-core:3.10.0' testImplementation 'org.mockito:mockito-inline:3.1.0' testImplementation 'androidx.test:core:1.2.0' testImplementation 'org.robolectric:robolectric:4.3.1' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9' } diff --git a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthCognito.kt b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthCognito.kt index 37781b8bdd..17fc176a26 100644 --- a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthCognito.kt +++ b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthCognito.kt @@ -22,6 +22,7 @@ import android.os.Handler import android.os.Looper import androidx.annotation.NonNull import androidx.annotation.VisibleForTesting +import com.amazonaws.amplify.amplify_auth_cognito.device.DeviceHandler import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignUpResult import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterSignInResult import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterFetchCognitoAuthSessionResult @@ -90,6 +91,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug var eventMessenger: BinaryMessenger? = null private lateinit var activityBinding: ActivityPluginBinding + /** + * Handles the Devices API. + */ + private val deviceHandler: DeviceHandler = DeviceHandler(errorHandler) + constructor() { authCognitoHubEventStreamHandler = AuthCognitoHubEventStreamHandler() } @@ -102,7 +108,7 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "com.amazonaws.amplify/auth_cognito") - channel.setMethodCallHandler(this); + channel.setMethodCallHandler(this) context = flutterPluginBinding.applicationContext; eventMessenger = flutterPluginBinding.getBinaryMessenger(); hubEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, @@ -156,6 +162,11 @@ public class AuthCognito : FlutterPlugin, ActivityAware, MethodCallHandler, Plug return } + if (DeviceHandler.canHandle(call.method)) { + deviceHandler.onMethodCall(call, result) + return + } + var data : HashMap = HashMap () try { data = checkData(checkArguments(call.arguments)); diff --git a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthErrorHandler.kt b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthErrorHandler.kt index 373a2bf085..f7c05b3547 100644 --- a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthErrorHandler.kt +++ b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/AuthErrorHandler.kt @@ -23,19 +23,7 @@ import com.amazonaws.amplify.amplify_auth_cognito.types.FlutterInvalidStateExcep import com.amazonaws.amplify.amplify_core.exception.ExceptionUtil import com.amazonaws.amplify.amplify_core.exception.ExceptionMessages import com.amazonaws.mobileconnectors.cognitoidentityprovider.exceptions.CognitoCodeExpiredException -import com.amazonaws.services.cognitoidentityprovider.model.InvalidLambdaResponseException -import com.amazonaws.services.cognitoidentityprovider.model.MFAMethodNotFoundException -import com.amazonaws.services.cognitoidentityprovider.model.NotAuthorizedException -import com.amazonaws.services.cognitoidentityprovider.model.SoftwareTokenMFANotFoundException -import com.amazonaws.services.cognitoidentityprovider.model.TooManyFailedAttemptsException -import com.amazonaws.services.cognitoidentityprovider.model.TooManyRequestsException -import com.amazonaws.services.cognitoidentityprovider.model.UnexpectedLambdaException -import com.amazonaws.services.cognitoidentityprovider.model.UserLambdaValidationException -import com.amazonaws.services.cognitoidentityprovider.model.LimitExceededException -import com.amazonaws.services.cognitoidentityprovider.model.InvalidParameterException -import com.amazonaws.services.cognitoidentityprovider.model.ExpiredCodeException -import com.amazonaws.services.cognitoidentityprovider.model.CodeMismatchException -import com.amazonaws.services.cognitoidentityprovider.model.CodeDeliveryFailureException +import com.amazonaws.services.cognitoidentityprovider.model.* import com.amplifyframework.AmplifyException import com.amplifyframework.auth.AuthException @@ -89,6 +77,7 @@ class AuthErrorHandler { is ExpiredCodeException -> errorCode = "CodeExpiredException" is CodeMismatchException -> errorCode = "CodeMismatchException" is CodeDeliveryFailureException -> errorCode = "CodeDeliveryFailureException" + is InvalidUserPoolConfigurationException -> errorCode = "ConfigurationException" } } } diff --git a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResult.kt b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResult.kt new file mode 100644 index 0000000000..b0e43cf1a5 --- /dev/null +++ b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResult.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.amplify.amplify_auth_cognito.base + +import io.flutter.Log +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Thread-safe [MethodChannel.Result] wrapper which prevents multiple replies and automatically posts + * results to the main thread. + */ +class AtomicResult(private val result: MethodChannel.Result, private val operation: String) : + MethodChannel.Result { + private companion object { + /** + * Scope for performing result handling. + * Method channel results must be sent on the main (UI) thread. + */ + val scope = MainScope() + } + + /** + * Whether a response has been sent. + */ + private val isSent = AtomicBoolean(false) + + override fun success(value: Any?) { + scope.launch { + if (isSent.getAndSet(true)) { + Log.w( + "AtomicResult(${operation})", + "Attempted to send success value after initial reply" + ) + return@launch + } + result.success(value) + } + } + + override fun error(errorCode: String?, errorMessage: String?, errorDetails: Any?) { + scope.launch { + if (isSent.getAndSet(true)) { + Log.w( + "AtomicResult(${operation})", + """ + Attempted to send error value after initial reply: + | PlatformException{code=${errorCode}, message=${errorMessage}, details=${errorDetails}} + """.trimMargin() + ) + return@launch + } + result.error(errorCode, errorMessage, errorDetails) + } + } + + override fun notImplemented() { + scope.launch { + if (isSent.getAndSet(true)) { + Log.w( + "AtomicResult(${operation})", + "Attempted to send notImplemented value after initial reply" + ) + return@launch + } + result.notImplemented() + } + } +} \ No newline at end of file diff --git a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/Device.kt b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/Device.kt new file mode 100644 index 0000000000..a064fbc423 --- /dev/null +++ b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/Device.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.amplify.amplify_auth_cognito.device + +import com.amazonaws.mobile.client.results.Device +import java.time.Instant +import java.util.* + +/** + * Attribute key for retrieving a [Device] instance's name. + */ +const val deviceNameKey = "device_name" + +/** + * The device's name, if set. + */ +val Device.deviceName: String? + get() = attributes?.get(deviceNameKey) + +/** + * Converts this device to a JSON-representable format. + */ +fun Device.toJson(): Map = mapOf( + "id" to deviceKey, + "name" to deviceName, + "attributes" to attributes, + "createdDate" to createDate?.time, + "lastModifiedDate" to lastModifiedDate?.time, + "lastAuthenticatedDate" to lastAuthenticatedDate?.time +) \ No newline at end of file diff --git a/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/DeviceHandler.kt b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/DeviceHandler.kt new file mode 100644 index 0000000000..482abdee31 --- /dev/null +++ b/packages/amplify_auth_cognito/android/src/main/kotlin/com/amazonaws/amplify/amplify_auth_cognito/device/DeviceHandler.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.amplify.amplify_auth_cognito.device + +import com.amazonaws.amplify.amplify_auth_cognito.AuthErrorHandler +import com.amazonaws.amplify.amplify_auth_cognito.base.AtomicResult +import com.amazonaws.mobile.client.AWSMobileClient +import com.amazonaws.mobile.client.Callback +import com.amazonaws.mobile.client.results.ListDevicesResult +import com.amplifyframework.auth.AuthDevice +import com.amplifyframework.auth.cognito.util.CognitoAuthExceptionConverter +import com.amplifyframework.core.Amplify +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.* + +/** + * Handles method calls for the Devices API. + */ +class DeviceHandler(private val errorHandler: AuthErrorHandler) : + MethodChannel.MethodCallHandler { + companion object { + /** + * Methods this handler supports. + */ + private val methods = listOf("rememberDevice", "forgetDevice", "fetchDevices") + + /** + * Whether this class can handle [method]. + */ + fun canHandle(method: String): Boolean = methods.contains(method) + } + + /** + * Scope for running asynchronous tasks. + */ + private val scope = CoroutineScope(Dispatchers.IO) + CoroutineName("DeviceHandler") + + @Suppress("UNCHECKED_CAST") + override fun onMethodCall(call: MethodCall, _result: MethodChannel.Result) { + val result = AtomicResult(_result, call.method) + when (call.method) { + "fetchDevices" -> fetchDevices(result) + "rememberDevice" -> rememberDevice(result) + "forgetDevice" -> { + val deviceJson = + (call.arguments as? Map<*, *> ?: emptyMap()) as Map + var device: AuthDevice? = null + if (deviceJson.isNotEmpty()) { + val id by deviceJson + device = AuthDevice.fromId(id as String) + } + forgetDevice(result, device) + } + } + } + + private fun fetchDevices(result: MethodChannel.Result) { + try { + val cognitoAuthPlugin = Amplify.Auth.getPlugin("awsCognitoAuthPlugin") + val awsMobileClient = cognitoAuthPlugin.escapeHatch as AWSMobileClient + scope.launch { + awsMobileClient.deviceOperations.list(object : Callback { + override fun onResult(listDevicesResult: ListDevicesResult) { + result.success(listDevicesResult.devices.map { it.toJson() }) + } + + override fun onError(exception: java.lang.Exception) { + errorHandler.handleAuthError( + result, CognitoAuthExceptionConverter.lookup( + exception, "Fetching devices failed." + ) + ) + } + }) + } + } catch (e: Exception) { + errorHandler.handleAuthError(result, e) + } + } + + private fun rememberDevice(result: MethodChannel.Result) { + scope.launch { + Amplify.Auth.rememberDevice( + { result.success(null) }, + { errorHandler.handleAuthError(result, it) } + ) + } + } + + private fun forgetDevice(result: MethodChannel.Result, device: AuthDevice? = null) { + scope.launch { + if (device != null) { + Amplify.Auth.forgetDevice( + device, + { result.success(null) }, + { errorHandler.handleAuthError(result, it) } + ) + } else { + Amplify.Auth.forgetDevice( + { result.success(null) }, + { errorHandler.handleAuthError(result, it) } + ) + } + } + } +} \ No newline at end of file diff --git a/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResultTest.kt b/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResultTest.kt new file mode 100644 index 0000000000..cb5559cf88 --- /dev/null +++ b/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/AtomicResultTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.amplify.amplify_auth_cognito.base + +import io.flutter.plugin.common.MethodChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.junit.MockitoJUnitRunner + +@ExperimentalCoroutinesApi +@RunWith(MockitoJUnitRunner::class) +class AtomicResultTest { + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + @Mock + private lateinit var mockResult: MethodChannel.Result + + @Test + fun successIsForwarded() = coroutinesTestRule.testDispatcher.runBlockingTest { + val atomicResult = AtomicResult(mockResult, "successIsForwarded") + atomicResult.success(null) + verify(mockResult).success(null) + } + + @Test + fun errorIsForwarded() = coroutinesTestRule.testDispatcher.runBlockingTest { + val atomicResult = AtomicResult(mockResult, "errorIsForwarded") + atomicResult.error(null, null, null) + verify(mockResult).error(null, null, null) + } + + @Test + fun notImplementedIsForwarded() = coroutinesTestRule.testDispatcher.runBlockingTest { + val atomicResult = AtomicResult(mockResult, "notImplementedIsForwarded") + atomicResult.notImplemented() + verify(mockResult).notImplemented() + } + + @Test + fun multipleSynchronousRepliesAreNotSent() = coroutinesTestRule.testDispatcher.runBlockingTest { + val atomicResult = AtomicResult(mockResult, "multipleSynchronousRepliesAreNotSent") + atomicResult.success(null) + atomicResult.success(null) + verify(mockResult, times(1)).success(any()) + } + + @Test + fun multipleConcurrentRepliesAreNotSent() = coroutinesTestRule.testDispatcher.runBlockingTest { + val atomicResult = AtomicResult(mockResult, "multipleConcurrentRepliesAreNotSent") + val jobs = mutableListOf() + for (i in 0..1000) { + val job = launch(Dispatchers.IO) { + atomicResult.success(null) + } + jobs.add(job) + } + // Block til jobs complete + jobs.forEach { + it.join() + } + verify(mockResult, times(1)).success(any()) + } +} \ No newline at end of file diff --git a/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/CoroutineTestRule.kt b/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/CoroutineTestRule.kt new file mode 100644 index 0000000000..a5690da803 --- /dev/null +++ b/packages/amplify_auth_cognito/android/src/test/kotlin/com/amazonaws/amplify/amplify_auth_cognito/base/CoroutineTestRule.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package com.amazonaws.amplify.amplify_auth_cognito.base + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * Test rule for running suspending tests on a common dispatcher. See [AtomicResultTest] for + * an example. + */ +@ExperimentalCoroutinesApi +class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : + TestWatcher() { + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + Dispatchers.resetMain() + testDispatcher.cleanupTestCoroutines() + } +} \ No newline at end of file diff --git a/packages/amplify_auth_cognito/example/ios/Runner.xcodeproj/project.pbxproj b/packages/amplify_auth_cognito/example/ios/Runner.xcodeproj/project.pbxproj index 4776bd483e..5bbfc2f0d5 100644 --- a/packages/amplify_auth_cognito/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/amplify_auth_cognito/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 26AECB412893A7A959622364 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE1727BF3FA0D464713D41A3 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4BD33A9326B483830051B8AC /* AtomicResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD33A9226B483830051B8AC /* AtomicResultTests.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 9674FCEAE95127916BA4C1B4 /* Pods_unit_tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D2AB19942AD94335DB089EE /* Pods_unit_tests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -43,6 +44,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3D23EF9D73AD2AD9798E569E /* Pods-amplify_auth_cognito_exampleTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-amplify_auth_cognito_exampleTests.profile.xcconfig"; path = "Target Support Files/Pods-amplify_auth_cognito_exampleTests/Pods-amplify_auth_cognito_exampleTests.profile.xcconfig"; sourceTree = ""; }; 3D2AB19942AD94335DB089EE /* Pods_unit_tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_unit_tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4BD33A9226B483830051B8AC /* AtomicResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AtomicResultTests.swift; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 77E8D0F580AB656481259C9E /* Pods-amplify_auth_cognito_exampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-amplify_auth_cognito_exampleTests.release.xcconfig"; path = "Target Support Files/Pods-amplify_auth_cognito_exampleTests/Pods-amplify_auth_cognito_exampleTests.release.xcconfig"; sourceTree = ""; }; @@ -141,6 +143,7 @@ children = ( 9C3D802725C1F52600728B7B /* amplify_auth_error_handling_tests.swift */, 9C404086251AA2430036C5FE /* MockAuthSession.swift */, + 4BD33A9226B483830051B8AC /* AtomicResultTests.swift */, B43589BC2581AA9600789DEE /* amplify_auth_cognito_tests.swift */, 9CEFDF1625113C2F001481FC /* Info.plist */, 9CC45C2325A4F7E90055E103 /* amplify_auth_cognito_hub_tests.swift */, @@ -401,6 +404,7 @@ buildActionMask = 2147483647; files = ( 9C3D802825C1F52600728B7B /* amplify_auth_error_handling_tests.swift in Sources */, + 4BD33A9326B483830051B8AC /* AtomicResultTests.swift in Sources */, 9CC45C2425A4F7E90055E103 /* amplify_auth_cognito_hub_tests.swift in Sources */, 9C404087251AA2430036C5FE /* MockAuthSession.swift in Sources */, 9C3D802B25C1F82800728B7B /* MockErrorConstants.swift in Sources */, diff --git a/packages/amplify_auth_cognito/example/ios/unit_tests/AtomicResultTests.swift b/packages/amplify_auth_cognito/example/ios/unit_tests/AtomicResultTests.swift new file mode 100644 index 0000000000..5bd2cf67c2 --- /dev/null +++ b/packages/amplify_auth_cognito/example/ios/unit_tests/AtomicResultTests.swift @@ -0,0 +1,87 @@ +// +// AtomicResultTests.swift +// unit_tests +// +// Created by Nys, Dillon on 7/30/21. +// + +import XCTest +import Flutter +@testable import amplify_auth_cognito + +class AtomicResultTests: XCTestCase { + + var counter: Int32 = 0 + + override func setUp() { + counter = 0 + } + + func test_value_is_forwarded() { + let expected = "value" + let exp = expectation(description: #function) + let result: FlutterResult = { (value: Any?) in + defer { exp.fulfill() } + guard let strValue = value as? String else { + XCTFail("Invalid value: \(value ?? "nil")") + return + } + XCTAssertEqual(strValue, expected) + } + let atomicResult = AtomicResult(result) + atomicResult(expected) + waitForExpectations(timeout: 1) + } + + func test_error_is_forwarded() { + let expected = FlutterError(code: "code", message: "message", details: nil) + let exp = expectation(description: #function) + let result: FlutterResult = { (value: Any?) in + defer { exp.fulfill() } + guard let errValue = value as? FlutterError else { + XCTFail("Invalid value: \(value ?? "nil")") + return + } + XCTAssertEqual(errValue, expected) + } + let atomicResult = AtomicResult(result) + atomicResult(expected) + waitForExpectations(timeout: 0.1) + } + + func test_multiple_synchronous_replies_are_not_sent() { + let result: FlutterResult = { _ in + OSAtomicIncrement32(&self.counter) + } + let atomicResult = AtomicResult(result) + atomicResult(nil) + atomicResult(nil) + let exp = expectation(description: #function) + DispatchQueue.main.async { [self] in + XCTAssertEqual(counter, 1) + exp.fulfill() + } + waitForExpectations(timeout: 0.1) + } + + func test_multiple_concurrent_replies_are_not_sent() { + let result: FlutterResult = { _ in + OSAtomicIncrement32(&self.counter) + } + let atomicResult = AtomicResult(result) + + DispatchQueue.global().sync { + DispatchQueue.concurrentPerform(iterations: 1000) { i in + atomicResult(nil) + } + } + + let exp = expectation(description: #function) + DispatchQueue.main.async { [self] in + XCTAssertEqual(counter, 1) + exp.fulfill() + } + + waitForExpectations(timeout: 0.2) + } +} diff --git a/packages/amplify_auth_cognito/ios/Classes/Base/AtomicResult.swift b/packages/amplify_auth_cognito/ios/Classes/Base/AtomicResult.swift new file mode 100644 index 0000000000..b3a2fe0d07 --- /dev/null +++ b/packages/amplify_auth_cognito/ios/Classes/Base/AtomicResult.swift @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import Foundation + +/// Thread-safe wrapper for [FlutterResult]. Prevents multiple replies and automically posts results to the main thread. +func AtomicResult(_ result: @escaping FlutterResult) -> FlutterResult { + return atomicResult(result).send +} + +private class atomicResult { + let result: FlutterResult + + /// Whether a reply has already been sent. + var isSent = false + + init(_ result: @escaping FlutterResult) { + self.result = result + } + + func send(_ value: Any?) { + DispatchQueue.main.async { [self] in + guard !isSent else { return } + result(value) + isSent = true + } + } +} diff --git a/packages/amplify_auth_cognito/ios/Classes/Device/AWSAuthDevice+Codable.swift b/packages/amplify_auth_cognito/ios/Classes/Device/AWSAuthDevice+Codable.swift new file mode 100644 index 0000000000..b9293a4a2b --- /dev/null +++ b/packages/amplify_auth_cognito/ios/Classes/Device/AWSAuthDevice+Codable.swift @@ -0,0 +1,61 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import Foundation +import Amplify +import AmplifyPlugins + +/// Adds JSON coding capabilities to [AWSAuthDevice]. +extension AWSAuthDevice: Codable { + /// Attribute key for retrieving a device's name. + static let deviceNameKey = "device_name" + + enum CodingKeys: String, CodingKey { + case id + case name + case attributes + case createdDate + case lastAuthenticatedDate + case lastModifiedDate + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let id = try container.decode(String.self, forKey: .id) + let name = try container.decode(String.self, forKey: .name) + let attributes = try container.decode([String: String]?.self, forKey: .attributes) + let createdDate = try container.decode(Date?.self, forKey: .createdDate) + let lastAuthenticatedDate = try container.decode(Date?.self, forKey: .lastAuthenticatedDate) + let lastModifiedDate = try container.decode(Date?.self, forKey: .lastModifiedDate) + self.init( + id: id, + name: name, + attributes: attributes, + createdDate: createdDate, + lastAuthenticatedDate: lastAuthenticatedDate, + lastModifiedDate: lastModifiedDate) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + // Do not use `name` since it is set by Amplify iOS to empty string. + try container.encode(attributes?[AWSAuthDevice.deviceNameKey], forKey: .name) + try container.encode(attributes, forKey: .attributes) + try container.encode(createdDate, forKey: .createdDate) + try container.encode(lastAuthenticatedDate, forKey: .lastAuthenticatedDate) + try container.encode(lastModifiedDate, forKey: .lastModifiedDate) + } +} diff --git a/packages/amplify_auth_cognito/ios/Classes/Device/DeviceHandler.swift b/packages/amplify_auth_cognito/ios/Classes/Device/DeviceHandler.swift new file mode 100644 index 0000000000..24649f1f09 --- /dev/null +++ b/packages/amplify_auth_cognito/ios/Classes/Device/DeviceHandler.swift @@ -0,0 +1,122 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import Foundation +import Flutter +import Amplify +import AmplifyPlugins + +/// Handles calls to the Devices API. +struct DeviceHandler { + /// Queue for handling asynchronous work. + private static let queue = DispatchQueue( + label: "com.awsamplify.flutter.auth.DeviceHandler", + qos: .utility, + attributes: [.concurrent]) + + /// Encoder used for proper [Date] handling. + private static let encoder = JSONEncoder(dateEncodingStrategy: .millisecondsSince1970) + + /// Decoder used for proper [Date] handling. + private static let decoder = JSONDecoder(dateDecodingStrategy: .millisecondsSince1970) + + /// Methods handled by [DeviceHandler]. + private static let methods: Set = ["rememberDevice", "forgetDevice", "fetchDevices"] + + /// Whether [DeviceHandler] can handle the given [method]. + static func canHandle(_ method: String) -> Bool { methods.contains(method) } + + let errorHandler: AuthErrorHandler + + func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard DeviceHandler.canHandle(call.method) else { return } + + let result = AtomicResult(result) + do { + switch call.method { + case "rememberDevice": + rememberDevice(result) + case "forgetDevice": + var awsDevice: AWSAuthDevice? = nil + if let deviceMap = call.arguments as? [String: Any] { + guard let deviceJSON = try? JSONSerialization.data(withJSONObject: deviceMap, options: []), + let device = try? DeviceHandler.decoder.decode(AWSAuthDevice.self, from: deviceJSON) else { + throw AuthError.validation( + "device", + "Invalid device JSON: \(deviceMap)", + "Please check that the device you're passing is a valid AuthDevice.", + nil) + } + awsDevice = device + } + forgetDevice(result, device: awsDevice) + case "fetchDevices": + fetchDevices(result) + default: + break + } + } catch let error as AuthError { + errorHandler.handleAuthError(authError: error, flutterResult: result) + } catch { + errorHandler.prepareGenericException(flutterResult: result, error: error) + } + } + + private func rememberDevice(_ flutterResult: @escaping FlutterResult) { + DeviceHandler.queue.async { + Amplify.Auth.rememberDevice { result in + switch result { + case .success(_): + flutterResult(nil) + case .failure(let authError): + errorHandler.handleAuthError(authError: authError, flutterResult: flutterResult) + } + } + } + } + + private func forgetDevice(_ flutterResult: @escaping FlutterResult, device: AWSAuthDevice? = nil) { + DeviceHandler.queue.async { + Amplify.Auth.forgetDevice(device) { result in + switch result { + case .success(_): + flutterResult(nil) + case .failure(let authError): + errorHandler.handleAuthError(authError: authError, flutterResult: flutterResult) + } + } + } + } + + private func fetchDevices(_ flutterResult: @escaping FlutterResult) { + DeviceHandler.queue.async { + Amplify.Auth.fetchDevices { result in + switch result { + case .success(let devices): + guard let awsAuthDevices = devices as? [AWSAuthDevice], + let devicesJSON = try? DeviceHandler.encoder.encode(awsAuthDevices), + let devicesMap = try? JSONSerialization.jsonObject(with: devicesJSON, options: []) else { + let error = AuthError.unknown("Error encoding devices: \(devices)", nil) + errorHandler.handleAuthError(authError: error, flutterResult: flutterResult) + return + } + flutterResult(devicesMap) + case .failure(let authError): + errorHandler.handleAuthError(authError: authError, flutterResult: flutterResult) + } + } + } + } +} diff --git a/packages/amplify_auth_cognito/ios/Classes/SwiftAuthCognito.swift b/packages/amplify_auth_cognito/ios/Classes/SwiftAuthCognito.swift index 5b43124b2c..10e769f8d3 100644 --- a/packages/amplify_auth_cognito/ios/Classes/SwiftAuthCognito.swift +++ b/packages/amplify_auth_cognito/ios/Classes/SwiftAuthCognito.swift @@ -28,10 +28,14 @@ public class SwiftAuthCognito: NSObject, FlutterPlugin { private let authCognitoHubEventStreamHandler: AuthCognitoHubEventStreamHandler? var errorHandler = AuthErrorHandler() + /// Handles calls to the Devices API. + private let deviceHandler: DeviceHandler + init(cognito: AuthCognitoBridge = AuthCognitoBridge(), authCognitoHubEventStreamHandler: AuthCognitoHubEventStreamHandler = AuthCognitoHubEventStreamHandler()) { self.cognito = cognito self.authCognitoHubEventStreamHandler = authCognitoHubEventStreamHandler + self.deviceHandler = DeviceHandler(errorHandler: errorHandler) } public static func register(with registrar: FlutterPluginRegistrar) { @@ -101,6 +105,11 @@ public class SwiftAuthCognito: NSObject, FlutterPlugin { } return } + + if (DeviceHandler.canHandle(call.method)) { + deviceHandler.handle(call, result: result) + return + } var arguments: Dictionary = [:] var data: NSMutableDictionary = [:] diff --git a/packages/amplify_auth_cognito/lib/amplify_auth_cognito.dart b/packages/amplify_auth_cognito/lib/amplify_auth_cognito.dart index ea4f81bfc3..67870cc759 100644 --- a/packages/amplify_auth_cognito/lib/amplify_auth_cognito.dart +++ b/packages/amplify_auth_cognito/lib/amplify_auth_cognito.dart @@ -15,6 +15,7 @@ import 'dart:async'; import 'dart:core'; +import 'package:amplify_auth_cognito/src/CognitoDevice/cognito_device.dart'; import 'package:amplify_auth_plugin_interface/amplify_auth_plugin_interface.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -146,4 +147,19 @@ class AmplifyAuthCognito extends AuthPluginInterface { await _instance.resendUserAttributeConfirmationCode(request: request); return res; } + + @override + Future rememberDevice() { + return _instance.rememberDevice(); + } + + @override + Future forgetDevice([AuthDevice? device]) { + return _instance.forgetDevice(device); + } + + @override + Future> fetchDevices() { + return _instance.fetchDevices(); + } } diff --git a/packages/amplify_auth_cognito/lib/amplify_auth_error_handling.dart b/packages/amplify_auth_cognito/lib/amplify_auth_error_handling.dart index d8e2d06022..56df5be87e 100644 --- a/packages/amplify_auth_cognito/lib/amplify_auth_error_handling.dart +++ b/packages/amplify_auth_cognito/lib/amplify_auth_error_handling.dart @@ -41,6 +41,10 @@ Exception castAndReturnPlatformException(PlatformException e) { return CodeMismatchException.fromMap( Map.from(e.details)); } + case 'ConfigurationException': + return InvalidUserPoolConfigurationException.fromMap(e.details as Map); + case 'DeviceNotTrackedException': + return DeviceNotTrackedException.fromMap(e.details as Map); case "FailedAttemptsLimitExceededException": { return FailedAttemptsLimitExceededException.fromMap( @@ -172,3 +176,16 @@ Exception castAndReturnPlatformException(PlatformException e) { } } } + +/// Transforms exceptions related to the Devices API. +Exception transformDeviceException(PlatformException e) { + final parsedException = castAndReturnPlatformException(e); + // Translate Android error to common exception. + if (parsedException is ResourceNotFoundException) { + return DeviceNotTrackedException( + recoverySuggestion: parsedException.recoverySuggestion, + underlyingException: parsedException.underlyingException, + ); + } + return parsedException; +} diff --git a/packages/amplify_auth_cognito/lib/method_channel_auth_cognito.dart b/packages/amplify_auth_cognito/lib/method_channel_auth_cognito.dart index bfddb40f30..908380c1af 100644 --- a/packages/amplify_auth_cognito/lib/method_channel_auth_cognito.dart +++ b/packages/amplify_auth_cognito/lib/method_channel_auth_cognito.dart @@ -504,4 +504,32 @@ class AmplifyAuthCognitoMethodChannel extends AmplifyAuthCognito { return ResendUserAttributeConfirmationCodeResult( codeDeliveryDetails: res["codeDeliveryDetails"]); } + + @override + Future rememberDevice() async { + try { + await _channel.invokeMethod('rememberDevice'); + } on PlatformException catch (e) { + throw transformDeviceException(e); + } + } + + @override + Future forgetDevice([AuthDevice? device]) async { + try { + await _channel.invokeMethod('forgetDevice', device?.toJson()); + } on PlatformException catch (e) { + throw transformDeviceException(e); + } + } + + @override + Future> fetchDevices() async { + try { + final devicesJson = await _channel.invokeListMethod('fetchDevices'); + return devicesJson?.map((e) => CognitoDevice.fromJson(e)).toList() ?? []; + } on PlatformException catch (e) { + throw transformDeviceException(e); + } + } } diff --git a/packages/amplify_auth_cognito/lib/src/CognitoDevice/cognito_device.dart b/packages/amplify_auth_cognito/lib/src/CognitoDevice/cognito_device.dart new file mode 100644 index 0000000000..6aced6c427 --- /dev/null +++ b/packages/amplify_auth_cognito/lib/src/CognitoDevice/cognito_device.dart @@ -0,0 +1,127 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; +import 'package:collection/collection.dart'; + +/// Helper functions for [AuthDevice]. +extension AuthDeviceX on AuthDevice { + /// Returns this device as a [CognitoDevice]. + CognitoDevice get asCognitoDevice => this is CognitoDevice + ? this as CognitoDevice + : CognitoDevice(id: id, name: name); +} + +/// {@template cognito_device} +/// A device tracked by AWS Cognito. +/// {@endtemplate} +class CognitoDevice extends AuthDevice { + /// Attribute key for retrieving a device's name. + static const deviceNameKey = 'device_name'; + + @override + final String id; + + /// Optional override of attribute value. + final String? _name; + + @override + String? get name => _name ?? attributes?[deviceNameKey]; + + /// Device attributes. + final Map? attributes; + + /// The date this device was created. + final DateTime? createdDate; + + /// The date this device was last authenticated. + final DateTime? lastAuthenticatedDate; + + /// The date this device was last updated. + final DateTime? lastModifiedDate; + + /// {@macro cognito_device} + const CognitoDevice({ + required this.id, + String? name, + this.attributes, + this.createdDate, + this.lastAuthenticatedDate, + this.lastModifiedDate, + }) : _name = name; + + factory CognitoDevice.fromJson(Map json) { + final attributes = json['attributes'] as Map?; + final createdDate = json['createdDate'] as int?; + final lastAuthenticatedDate = json['lastAuthenticatedDate'] as int?; + final lastModifiedDate = json['lastModifiedDate'] as int?; + return CognitoDevice( + id: json['id'] as String, + name: json['name'] as String?, + attributes: + attributes == null ? null : Map.from(attributes), + createdDate: createdDate == null + ? null + : DateTime.fromMillisecondsSinceEpoch(createdDate), + lastAuthenticatedDate: lastAuthenticatedDate == null + ? null + : DateTime.fromMillisecondsSinceEpoch(lastAuthenticatedDate), + lastModifiedDate: lastModifiedDate == null + ? null + : DateTime.fromMillisecondsSinceEpoch(lastModifiedDate), + ); + } + + @override + Map toJson() => { + 'id': id, + 'name': name, + if (attributes != null) 'attributes': attributes, + if (createdDate != null) + 'createdDate': createdDate!.millisecondsSinceEpoch, + if (lastAuthenticatedDate != null) + 'lastAuthenticatedDate': + lastAuthenticatedDate!.millisecondsSinceEpoch, + if (lastModifiedDate != null) + 'lastModifiedDate': lastModifiedDate!.millisecondsSinceEpoch, + }; + + @override + String toString() { + return 'CognitoDevice{id=$id, name=$name, attributes=$attributes, createdDate=$createdDate, ' + 'lastAuthenticatedDate=$lastAuthenticatedDate, lastModifiedDate=$lastModifiedDate}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CognitoDevice && + id == other.id && + name == other.name && + const MapEquality().equals(attributes, other.attributes) && + createdDate == other.createdDate && + lastAuthenticatedDate == other.lastAuthenticatedDate && + lastModifiedDate == other.lastModifiedDate; + + @override + int get hashCode => const DeepCollectionEquality().hash([ + id, + name, + attributes, + createdDate, + lastAuthenticatedDate, + lastModifiedDate, + ]); +} diff --git a/packages/amplify_auth_cognito/lib/src/types.dart b/packages/amplify_auth_cognito/lib/src/types.dart index df925b266e..a15374ebc8 100644 --- a/packages/amplify_auth_cognito/lib/src/types.dart +++ b/packages/amplify_auth_cognito/lib/src/types.dart @@ -35,6 +35,9 @@ export 'CognitoSession/AWSCredentials.dart'; export 'CognitoSession/CognitoAuthSession.dart'; export 'CognitoSession/CognitoSessionOptions.dart'; +// Device +export 'CognitoDevice/cognito_device.dart'; + // Exceptions export 'package:amplify_auth_plugin_interface/src/Exceptions/AuthException.dart'; export 'package:amplify_auth_plugin_interface/src/Exceptions/AliasExistsException.dart'; diff --git a/packages/amplify_auth_plugin_interface/lib/amplify_auth_plugin_interface.dart b/packages/amplify_auth_plugin_interface/lib/amplify_auth_plugin_interface.dart index fc061d4ba3..7194a446b3 100644 --- a/packages/amplify_auth_plugin_interface/lib/amplify_auth_plugin_interface.dart +++ b/packages/amplify_auth_plugin_interface/lib/amplify_auth_plugin_interface.dart @@ -116,4 +116,19 @@ abstract class AuthPluginInterface extends AmplifyPluginInterface { throw UnimplementedError( 'resendUserAttributeConfirmationCode() has not been implemented.'); } + + /// Remembers the current device. + Future rememberDevice() { + throw UnimplementedError('rememberDevice() has not been implemented.'); + } + + /// Forgets [device], or the current device, if no parameters are given. + Future forgetDevice([AuthDevice? device]) { + throw UnimplementedError('forgetDevice() has not been implemented.'); + } + + /// Retrieves all tracked devices for the current user. + Future> fetchDevices() { + throw UnimplementedError('fetchDevices() has not been implemented.'); + } } diff --git a/packages/amplify_auth_plugin_interface/lib/src/Exceptions/AuthException.dart b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/AuthException.dart index ec0bd9b2aa..024240f539 100644 --- a/packages/amplify_auth_plugin_interface/lib/src/Exceptions/AuthException.dart +++ b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/AuthException.dart @@ -17,7 +17,7 @@ import 'package:amplify_core/types/exception/AmplifyException.dart'; /// Base Class for Auth Exceptions class AuthException extends AmplifyException { - AuthException(String message, + const AuthException(String message, {String? recoverySuggestion, String? underlyingException}) : super(message, recoverySuggestion: recoverySuggestion, diff --git a/packages/amplify_auth_plugin_interface/lib/src/Exceptions/device_not_tracked_exception.dart b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/device_not_tracked_exception.dart new file mode 100644 index 0000000000..4692090f2a --- /dev/null +++ b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/device_not_tracked_exception.dart @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import 'package:amplify_auth_plugin_interface/amplify_auth_plugin_interface.dart'; + +/// {@template device_not_tracked_exception} +/// Exception thrown when a request is made for a device which is either not currently tracked +/// or previously forgotten. +/// {@endtemplate} +class DeviceNotTrackedException extends AuthException { + /// {@macro device_not_tracked_exception} + const DeviceNotTrackedException( + {String? recoverySuggestion, String? underlyingException}) + : super( + 'This device does not have an id, either it was never tracked or previously forgotten.', + recoverySuggestion: recoverySuggestion, + underlyingException: underlyingException, + ); + + static DeviceNotTrackedException fromMap(Map map) => + DeviceNotTrackedException( + recoverySuggestion: map['recoverySuggestion'] as String?, + underlyingException: map['underlyingException'] as String?, + ); +} diff --git a/packages/amplify_auth_plugin_interface/lib/src/Exceptions/invalid_user_pool_configuration_exception.dart b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/invalid_user_pool_configuration_exception.dart new file mode 100644 index 0000000000..c610cf1f42 --- /dev/null +++ b/packages/amplify_auth_plugin_interface/lib/src/Exceptions/invalid_user_pool_configuration_exception.dart @@ -0,0 +1,24 @@ +import 'package:amplify_auth_plugin_interface/amplify_auth_plugin_interface.dart'; + +/// {@template invalid_user_pool_configuration_exception} +/// Thrown when a user pool is is not configured for the requested action. +/// {@endtemplate} +class InvalidUserPoolConfigurationException extends AuthException { + /// {@macro invalid_user_pool_configuration_exception} + const InvalidUserPoolConfigurationException({ + required String message, + String? recoverySuggestion, + String? underlyingException, + }) : super( + message, + recoverySuggestion: recoverySuggestion, + underlyingException: underlyingException, + ); + + static InvalidUserPoolConfigurationException fromMap(Map map) => + InvalidUserPoolConfigurationException( + message: map['message'] as String, + recoverySuggestion: map['recoverySuggestion'] as String?, + underlyingException: map['underlyingException'] as String?, + ); +} diff --git a/packages/amplify_auth_plugin_interface/lib/src/types.dart b/packages/amplify_auth_plugin_interface/lib/src/types.dart index 1c9426b0f0..7e1264d63c 100644 --- a/packages/amplify_auth_plugin_interface/lib/src/types.dart +++ b/packages/amplify_auth_plugin_interface/lib/src/types.dart @@ -71,12 +71,14 @@ export 'Exceptions/AliasExistsException.dart'; export 'Exceptions/CodeDeliveryFailureException.dart'; export 'Exceptions/CodeExpiredException.dart'; export 'Exceptions/CodeMismatchException.dart'; +export 'Exceptions/device_not_tracked_exception.dart'; export 'Exceptions/FailedAttemptsLimitExceededException.dart'; export 'Exceptions/InternalErrorException.dart'; export 'Exceptions/InvalidAccountTypeException.dart'; export 'Exceptions/InvalidParameterException.dart'; export 'Exceptions/InvalidPasswordException.dart'; export 'Exceptions/InvalidStateException.dart'; +export 'Exceptions/invalid_user_pool_configuration_exception.dart'; export 'Exceptions/LambdaException.dart'; export 'Exceptions/LimitExceededException.dart'; export 'Exceptions/MFAMethodNotFoundException.dart'; @@ -99,3 +101,4 @@ export 'Exceptions/UserNotFoundException.dart'; // Utility Classes export 'types/AuthCodeDeliveryDetails.dart'; export 'types/AuthNextStep.dart'; +export 'types/auth_device.dart'; diff --git a/packages/amplify_auth_plugin_interface/lib/src/types/auth_device.dart b/packages/amplify_auth_plugin_interface/lib/src/types/auth_device.dart new file mode 100644 index 0000000000..e963ed0efe --- /dev/null +++ b/packages/amplify_auth_plugin_interface/lib/src/types/auth_device.dart @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/// @{template auth_device} +/// Common interface for devices tracked by an authentication provider. +/// @{end_template} +abstract class AuthDevice { + /// {@macro auth_device} + const AuthDevice(); + + /// Device unique identifier. + String get id; + + /// Device name. + String? get name; + + /// Converts the instance to a JSON map. + Map toJson(); + + @override + String toString() { + return 'AuthDevice{id=$id, name=$name}'; + } +} diff --git a/packages/amplify_flutter/lib/categories/amplify_auth_category.dart b/packages/amplify_flutter/lib/categories/amplify_auth_category.dart index 0e5fa0cd45..f2fc639038 100644 --- a/packages/amplify_flutter/lib/categories/amplify_auth_category.dart +++ b/packages/amplify_flutter/lib/categories/amplify_auth_category.dart @@ -209,4 +209,25 @@ class AuthCategory { ? plugins[0].resendUserAttributeConfirmationCode(request: request) : throw _pluginNotAddedException("Auth"); } + + /// Remembers the current device. + Future rememberDevice() { + return plugins.length == 1 + ? plugins[0].rememberDevice() + : throw _pluginNotAddedException("Auth"); + } + + /// Forgets [device], or the current device, if no parameters are given. + Future forgetDevice([AuthDevice? device]) { + return plugins.length == 1 + ? plugins[0].forgetDevice(device) + : throw _pluginNotAddedException("Auth"); + } + + /// Retrieves all tracked devices for the current user. + Future> fetchDevices() { + return plugins.length == 1 + ? plugins[0].fetchDevices() + : throw _pluginNotAddedException("Auth"); + } }