diff --git a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift index 6dfafe2c1b..4ebe4bcb5a 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift @@ -88,7 +88,7 @@ import Foundation /// directly by host applications. The behavior of this may change without warning. public enum ModelAssociation { case hasMany(associatedFieldName: String?, associatedFieldNames: [String] = []) - case hasOne(associatedFieldName: String?, targetNames: [String]) + case hasOne(associatedFieldName: String?, associatedFieldNames: [String] = [], targetNames: [String]) case belongsTo(associatedFieldName: String?, targetNames: [String]) public static let belongsTo: ModelAssociation = .belongsTo(associatedFieldName: nil, targetNames: []) @@ -108,14 +108,20 @@ public enum ModelAssociation { ) } - @available(*, deprecated, message: "Use hasOne(associatedWith:targetNames:)") + @available(*, deprecated, message: "Use hasOne(associatedWith:associatedFields:targetNames:)") public static func hasOne(associatedWith: CodingKey?, targetName: String? = nil) -> ModelAssociation { let targetNames = targetName.map { [$0] } ?? [] return .hasOne(associatedWith: associatedWith, targetNames: targetNames) } - public static func hasOne(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation { - return .hasOne(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames) + public static func hasOne( + associatedWith: CodingKey? = nil, + associatedFields: [CodingKey] = [], + targetNames: [String] = []) -> ModelAssociation { + return .hasOne( + associatedFieldName: associatedWith?.stringValue, + associatedFieldNames: associatedFields.map { $0.stringValue }, + targetNames: targetNames) } @available(*, deprecated, message: "Use belongsTo(associatedWith:targetNames:)") @@ -254,7 +260,7 @@ extension ModelField { let associatedModel = requiredAssociatedModelName switch association { case .belongsTo(let associatedKey, _), - .hasOne(let associatedKey, _), + .hasOne(let associatedKey, _, _), .hasMany(let associatedKey, _): // TODO handle modelName casing (convert to camelCase) let key = associatedKey ?? associatedModel diff --git a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift index b27d40bb55..35e6f1210d 100644 --- a/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift +++ b/Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift @@ -279,6 +279,21 @@ public enum ModelFieldDefinition { association: .hasOne(associatedWith: associatedKey, targetNames: targetNames)) } + public static func hasOne(_ key: CodingKey, + is nullability: ModelFieldNullability = .required, + isReadOnly: Bool = false, + ofType type: Model.Type, + associatedFields associatedKeys: [CodingKey], + targetNames: [String] = []) -> ModelFieldDefinition { + return .field(key, + is: nullability, + isReadOnly: isReadOnly, + ofType: .model(type: type), + association: .hasOne(associatedWith: associatedKeys.first, + associatedFields: associatedKeys, + targetNames: targetNames)) + } + public static func belongsTo(_ key: CodingKey, is nullability: ModelFieldNullability = .required, isReadOnly: Bool = false, diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift index 4f1eaab6f0..19f1368dd6 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Core/AppSyncListProvider.swift @@ -268,7 +268,7 @@ public class AppSyncListProvider: ModelListProvider { } let defaultFieldName = modelSchema.name.camelCased() + field.pascalCased() + "Id" switch modelField.association { - case .belongsTo(_, let targetNames), .hasOne(_, let targetNames): + case .belongsTo(_, let targetNames), .hasOne(_, _, let targetNames): guard !targetNames.isEmpty else { return [defaultFieldName] diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift index b153fe844f..56b085c0c3 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginGen2GraphQLTests/Gen2_4/Customer4+Schema.swift @@ -32,7 +32,7 @@ extension Customer4 { model.fields( .field(customer4.id, is: .required, ofType: .string), .field(customer4.name, is: .optional, ofType: .string), - .hasOne(customer4.activeCart, is: .optional, ofType: Cart4.self, associatedWith: Cart4.keys.customer), + .hasOne(customer4.activeCart, is: .optional, ofType: Cart4.self, associatedFields: [Cart4.keys.customer]), .field(customer4.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), .field(customer4.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) ) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignOut/ShowHostedUISignOut.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignOut/ShowHostedUISignOut.swift index 6d64dbf93a..4ff593f96d 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignOut/ShowHostedUISignOut.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/SignOut/ShowHostedUISignOut.swift @@ -26,17 +26,14 @@ class ShowHostedUISignOut: NSObject, Action { guard let environment = environment as? AuthEnvironment, let hostedUIEnvironment = environment.hostedUIEnvironment else { - let message = AuthPluginErrorConstants.configurationError - let error = AuthenticationError.configuration(message: message) + let error = HostedUIError.pluginConfiguration(AuthPluginErrorConstants.configurationError) await sendEvent(with: error, dispatcher: dispatcher, environment: environment) return } let hostedUIConfig = hostedUIEnvironment.configuration - guard let callbackURL = URL(string: hostedUIConfig.oauth.signOutRedirectURI), let callbackURLScheme = callbackURL.scheme else { - let error = AuthenticationError.configuration(message: "Callback URL could not be retrieved") - await sendEvent(with: error, dispatcher: dispatcher, environment: environment) + await sendEvent(with: HostedUIError.signOutRedirectURI, dispatcher: dispatcher, environment: environment) return } @@ -48,13 +45,7 @@ class ShowHostedUISignOut: NSObject, Action { callbackScheme: callbackURLScheme, inPrivate: false, presentationAnchor: signOutEvent.presentationAnchor) - await sendEvent(with: nil, dispatcher: dispatcher, environment: environment) - - } catch HostedUIError.signOutURI { - let error = AuthenticationError.configuration(message: "Could not create logout URL") - await sendEvent(with: error, dispatcher: dispatcher, environment: environment) - return } catch { self.logVerbose("\(#fileID) Received error \(error)", environment: environment) await sendEvent(with: error, dispatcher: dispatcher, environment: environment) @@ -65,32 +56,33 @@ class ShowHostedUISignOut: NSObject, Action { dispatcher: EventDispatcher, environment: Environment) async { - var hostedUIError: AWSCognitoHostedUIError? - if let hostedUIInternalError = error as? HostedUIError, - case .cancelled = hostedUIInternalError { - let event = SignOutEvent(eventType: .userCancelled) - self.logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) - return - } - - if let error = error as? AuthErrorConvertible { - hostedUIError = AWSCognitoHostedUIError(error: error.authError) + let event: SignOutEvent + if let hostedUIInternalError = error as? HostedUIError { + event = SignOutEvent(eventType: .hostedUISignOutError(hostedUIInternalError)) + } else if let error = error as? AuthErrorConvertible { + event = getEvent(for: AWSCognitoHostedUIError(error: error.authError)) } else if let error = error { - let serviceError = AuthError.service("HostedUI failed with error", - "", error) - hostedUIError = AWSCognitoHostedUIError(error: serviceError) + let serviceError = AuthError.service( + "HostedUI failed with error", + "", + error + ) + event = getEvent(for: AWSCognitoHostedUIError(error: serviceError)) + } else { + event = getEvent(for: nil) } - let event: SignOutEvent + self.logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) + await dispatcher.send(event) + } + + private func getEvent(for hostedUIError: AWSCognitoHostedUIError?) -> SignOutEvent { if self.signOutEvent.globalSignOut { - event = SignOutEvent(eventType: .signOutGlobally(self.signInData, + return SignOutEvent(eventType: .signOutGlobally(self.signInData, hostedUIError: hostedUIError)) } else { - event = SignOutEvent(eventType: .revokeToken(self.signInData, + return SignOutEvent(eventType: .revokeToken(self.signInData, hostedUIError: hostedUIError)) } - self.logVerbose("\(#fileID) Sending event \(event.type)", environment: environment) - await dispatcher.send(event) } } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift index d39751956a..b4a7aa9d4a 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/FetchAuthSessionOperationHelper.swift @@ -66,23 +66,21 @@ class FetchAuthSessionOperationHelper: DefaultLogger { authStateMachine: AuthStateMachine, forceRefresh: Bool) async throws -> AuthSession { - var event: AuthorizationEvent if forceRefresh || !credentials.areValid() { - if case .identityPoolWithFederation( - let federatedToken, - let identityId, - _ - ) = credentials { - event = AuthorizationEvent( - eventType: .startFederationToIdentityPool(federatedToken, identityId) - ) - } else { + var event: AuthorizationEvent + switch credentials { + case .identityPoolWithFederation(let federatedToken, let identityId, _): + event = AuthorizationEvent(eventType: .startFederationToIdentityPool(federatedToken, identityId)) + case .noCredentials: + event = AuthorizationEvent(eventType: .fetchUnAuthSession) + case .userPoolOnly, .identityPoolOnly, .userPoolAndIdentityPool: event = AuthorizationEvent(eventType: .refreshSession(forceRefresh)) } await authStateMachine.send(event) return try await listenForSession(authStateMachine: authStateMachine) + } else { + return credentials.cognitoSession } - return credentials.cognitoSession } func listenForSession(authStateMachine: AuthStateMachine) async throws -> AuthSession { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/HostedUIError.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/HostedUIError.swift index d8ba2e8ef1..d87ec3c106 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/HostedUIError.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/HostedUIError.swift @@ -15,6 +15,8 @@ enum HostedUIError: Error { case signOutURI + case signOutRedirectURI + case proofCalculation case codeValidation @@ -23,6 +25,8 @@ enum HostedUIError: Error { case serviceMessage(String) + case pluginConfiguration(String) + case cancelled case invalidContext diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/SignOutError.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/SignOutError.swift index cfd962d247..7aaff48375 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/SignOutError.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Errors/SignOutError.swift @@ -10,7 +10,7 @@ import Amplify enum SignOutError: Error { - case userCancelled + case hostedUI(HostedUIError) case localSignOut } @@ -18,10 +18,22 @@ enum SignOutError: Error { extension SignOutError: AuthErrorConvertible { var authError: AuthError { switch self { - case .userCancelled: - return AuthError.service("", "", AWSCognitoAuthError.userCancelled) + case .hostedUI(let error): + return error.authError case .localSignOut: return AuthError.unknown("", nil) } } } + +extension SignOutError: Equatable { + static func == (lhs: SignOutError, rhs: SignOutError) -> Bool { + switch (lhs, rhs) { + case (.hostedUI, .hostedUI), + (.localSignOut, .localSignOut): + return true + default: + return false + } + } +} diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignOutEvent.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignOutEvent.swift index 4d30ec5a38..3d223e5a7a 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignOutEvent.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Events/SignOutEvent.swift @@ -41,7 +41,7 @@ struct SignOutEvent: StateMachineEvent { case signedOutFailure(AuthenticationError) - case userCancelled + case hostedUISignOutError(HostedUIError) } let id: String @@ -66,8 +66,8 @@ struct SignOutEvent: StateMachineEvent { return "SignOutEvent.globalSignOutError" case .signOutGuest: return "SignOutEvent.signOutGuest" - case .userCancelled: - return "SignOutEvent.userCancelled" + case .hostedUISignOutError: + return "SignOutEvent.hostedUISignOutError" } } @@ -94,7 +94,7 @@ extension SignOutEvent.EventType: Equatable { (.signedOutFailure, .signedOutFailure), (.globalSignOutError, .globalSignOutError), (.signOutGuest, .signOutGuest), - (.userCancelled, .userCancelled): + (.hostedUISignOutError, .hostedUISignOutError): return true default: return false diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/ErrorMapping/SignInError+Helper.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/ErrorMapping/SignInError+Helper.swift index 2a5630d898..a6e21bced9 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/ErrorMapping/SignInError+Helper.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/ErrorMapping/SignInError+Helper.swift @@ -74,10 +74,15 @@ extension HostedUIError: AuthErrorConvertible { AuthPluginErrorConstants.hostedUITokenURI.recoverySuggestion) case .signOutURI: - return .service( + return .configuration( AuthPluginErrorConstants.hostedUISignOutURI.errorDescription, AuthPluginErrorConstants.hostedUISignOutURI.recoverySuggestion) + case .signOutRedirectURI: + return .configuration( + AuthPluginErrorConstants.hostedUISignOutRedirectURI.errorDescription, + AuthPluginErrorConstants.hostedUISignOutRedirectURI.recoverySuggestion) + case .proofCalculation: return .invalidState( AuthPluginErrorConstants.hostedUIProofCalculation.errorDescription, @@ -107,11 +112,15 @@ extension HostedUIError: AuthErrorConvertible { case .unableToStartASWebAuthenticationSession: return .service( AuthPluginErrorConstants.hostedUIUnableToStartASWebAuthenticationSession.errorDescription, - AuthPluginErrorConstants.hostedUIUnableToStartASWebAuthenticationSession.recoverySuggestion) + AuthPluginErrorConstants.hostedUIUnableToStartASWebAuthenticationSession.recoverySuggestion, + AWSCognitoAuthError.errorLoadingUI) case .serviceMessage(let message): return .service(message, AuthPluginErrorConstants.serviceError) + case .pluginConfiguration(let message): + return .configuration(message, AuthPluginErrorConstants.configurationError) + case .unknown: return .unknown("WebUI signIn encountered an unknown error", nil) diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignOut/SignOutState+Resolver.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignOut/SignOutState+Resolver.swift index 37e5ec4f04..17523862b4 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignOut/SignOutState+Resolver.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/Resolvers/SignOut/SignOutState+Resolver.swift @@ -117,9 +117,9 @@ extension SignOutState { newState: SignOutState.revokingToken, actions: [action] ) - case .userCancelled: + case .hostedUISignOutError(let error): let action = CancelSignOut(signedInData: signedInData) - return .init(newState: .error(.userCancelled), actions: [action]) + return .init(newState: .error(.hostedUI(error)), actions: [action]) default: return .from(oldState) } diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Constants/AuthPluginErrorConstants.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Constants/AuthPluginErrorConstants.swift index b6bbdfff89..ab899e5682 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Constants/AuthPluginErrorConstants.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Constants/AuthPluginErrorConstants.swift @@ -56,6 +56,10 @@ enum AuthPluginErrorConstants { "SignOut URI could not be created", "Check the configuration to make sure that HostedUI related information are present") + static let hostedUISignOutRedirectURI: AuthPluginErrorString = ( + "Callback URL could not be retrieved", + "Check the configuration to make sure that HostedUI related information are present") + static let hostedUIProofCalculation: AuthPluginErrorString = ( "Proof calculation failed", "Reach out with amplify team via github to raise an issue") diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift index e00f5a4b3e..af2129ad19 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ActionTests/SignOut/ShowHostedUISignOutTests.swift @@ -114,7 +114,7 @@ class ShowHostedUISignOutTests: XCTestCase { return } - XCTAssertEqual(event.eventType, .userCancelled) + XCTAssertEqual(event.eventType, .hostedUISignOutError(.cancelled)) expectation.fulfill() }, environment: Defaults.makeDefaultAuthEnvironment( @@ -142,21 +142,19 @@ class ShowHostedUISignOutTests: XCTestCase { await action.execute( withDispatcher: MockDispatcher { event in guard let event = event as? SignOutEvent, - case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + case .hostedUISignOutError(let hostedUIError) = event.eventType else { XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") expectation.fulfill() return } - guard let hostedUIError = hostedUIError, - case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + guard case .configuration(let errorDescription, _, let serviceError) = hostedUIError.authError else { XCTFail("Expected AuthError.configuration") expectation.fulfill() return } - XCTAssertEqual(errorDescription, "Could not create logout URL") - XCTAssertEqual(data, signInData) + XCTAssertEqual(errorDescription, "SignOut URI could not be created") XCTAssertNil(serviceError) expectation.fulfill() }, @@ -185,14 +183,13 @@ class ShowHostedUISignOutTests: XCTestCase { await action.execute( withDispatcher: MockDispatcher { event in guard let event = event as? SignOutEvent, - case .signOutGlobally(let data, let hostedUIError) = event.eventType else { - XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") + case .hostedUISignOutError(let hostedUIError) = event.eventType else { + XCTFail("Expected SignOutEvent.hostedUISignOutError, got \(event)") expectation.fulfill() return } - guard let hostedUIError = hostedUIError, - case .invalidState(let errorDescription, let recoverySuggestion, let serviceError) = hostedUIError.error else { + guard case .invalidState(let errorDescription, let recoverySuggestion, let serviceError) = hostedUIError.authError else { XCTFail("Expected AuthError.invalidState") expectation.fulfill() return @@ -200,7 +197,6 @@ class ShowHostedUISignOutTests: XCTestCase { XCTAssertEqual(errorDescription, AuthPluginErrorConstants.hostedUIInvalidPresentation.errorDescription) XCTAssertEqual(recoverySuggestion, AuthPluginErrorConstants.hostedUIInvalidPresentation.recoverySuggestion) - XCTAssertEqual(data, signInData) XCTAssertNil(serviceError) expectation.fulfill() }, @@ -229,21 +225,19 @@ class ShowHostedUISignOutTests: XCTestCase { await action.execute( withDispatcher: MockDispatcher { event in guard let event = event as? SignOutEvent, - case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + case .hostedUISignOutError(let hostedUIError) = event.eventType else { XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") expectation.fulfill() return } - guard let hostedUIError = hostedUIError, - case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + guard case .configuration(let errorDescription, _, let serviceError) = hostedUIError.authError else { XCTFail("Expected AuthError.configuration") expectation.fulfill() return } XCTAssertEqual(errorDescription, "Callback URL could not be retrieved") - XCTAssertEqual(data, signInData) XCTAssertNil(serviceError) expectation.fulfill() }, @@ -269,20 +263,18 @@ class ShowHostedUISignOutTests: XCTestCase { await action.execute( withDispatcher: MockDispatcher { event in guard let event = event as? SignOutEvent, - case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + case .hostedUISignOutError(let hostedUIError) = event.eventType else { XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") expectation.fulfill() return } - guard let hostedUIError = hostedUIError, - case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + guard case .configuration(let errorDescription, _, let serviceError) = hostedUIError.authError else { XCTFail("Expected AuthError.configuration") expectation.fulfill() return } - XCTAssertEqual(data, signInData) XCTAssertEqual(errorDescription, AuthPluginErrorConstants.configurationError) XCTAssertNil(serviceError) expectation.fulfill() @@ -309,20 +301,18 @@ class ShowHostedUISignOutTests: XCTestCase { await action.execute( withDispatcher: MockDispatcher { event in guard let event = event as? SignOutEvent, - case .signOutGlobally(let data, let hostedUIError) = event.eventType else { + case .hostedUISignOutError(let hostedUIError) = event.eventType else { XCTFail("Expected SignOutEvent.signOutGlobally, got \(event)") expectation.fulfill() return } - guard let hostedUIError = hostedUIError, - case .configuration(let errorDescription, _, let serviceError) = hostedUIError.error else { + guard case .configuration(let errorDescription, _, let serviceError) = hostedUIError.authError else { XCTFail("Expected AuthError.configuration") expectation.fulfill() return } - XCTAssertEqual(data, signInData) XCTAssertEqual(errorDescription, AuthPluginErrorConstants.configurationError) XCTAssertNil(serviceError) expectation.fulfill() diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Mocks/MockData/SignedInData+Mock.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Mocks/MockData/SignedInData+Mock.swift index efd34aa2de..e5520078d6 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Mocks/MockData/SignedInData+Mock.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Mocks/MockData/SignedInData+Mock.swift @@ -23,4 +23,15 @@ extension SignedInData { signInMethod: .apiBased(.userSRP), cognitoUserPoolTokens: tokens) } + + static var hostedUISignInData: SignedInData { + let tokens = AWSCognitoUserPoolTokens.testData + return SignedInData(signedInDate: Date(), + signInMethod: .hostedUI(.init( + scopes: [], + providerInfo: .init(authProvider: .google, idpIdentifier: ""), + presentationAnchor: nil, + preferPrivateSession: false)), + cognitoUserPoolTokens: tokens) + } } diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/AuthorizationState/AuthorizationTestData.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/AuthorizationState/AuthorizationTestData.swift index c8a4461240..0eb909095e 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/AuthorizationState/AuthorizationTestData.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/AuthorizationState/AuthorizationTestData.swift @@ -53,6 +53,12 @@ extension AmplifyCredentials { credentials: .testData) } + static var hostedUITestData: AmplifyCredentials { + AmplifyCredentials.userPoolAndIdentityPool(signedInData: .hostedUISignInData, + identityID: "identityId", + credentials: AuthAWSCognitoCredentials.testData) + } + static var testDataWithExpiredTokens: AmplifyCredentials { AmplifyCredentials.userPoolAndIdentityPool(signedInData: .expiredTestData, identityID: "identityId", diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateNotStartedTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateNotStartedTests.swift index 46d52736d7..a243505604 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateNotStartedTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateNotStartedTests.swift @@ -22,7 +22,7 @@ class SignOutStateNotStartedTests: XCTestCase { case .signOutLocally, .signedOutSuccess, .signedOutFailure, - .userCancelled, + .hostedUISignOutError, .globalSignOutError: XCTAssertEqual( resolver.resolve( diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateRevokingTokenTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateRevokingTokenTests.swift index 0d858412c1..11a030dbc4 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateRevokingTokenTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateRevokingTokenTests.swift @@ -24,7 +24,7 @@ class SignOutStateRevokingTokenTests: XCTestCase { .signedOutSuccess, .invokeHostedUISignOut, .signOutGuest, - .userCancelled, + .hostedUISignOutError, .globalSignOutError: XCTAssertEqual( resolver.resolve( diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutGloballyTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutGloballyTests.swift index dd52615f57..fe163143d3 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutGloballyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutGloballyTests.swift @@ -24,7 +24,7 @@ class SignOutStateSigningOutGloballyTests: XCTestCase { .signedOutSuccess, .invokeHostedUISignOut, .signOutGuest, - .userCancelled, + .hostedUISignOutError, .signedOutFailure: XCTAssertEqual( resolver.resolve( diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutLocallyTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutLocallyTests.swift index 8394439384..31e0495e77 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutLocallyTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/ResolverTests/SignOutState/SignOutStateSigningOutLocallyTests.swift @@ -24,7 +24,7 @@ class SignOutStateSigningOutLocallyTests: XCTestCase { .signOutLocally, .invokeHostedUISignOut, .signOutGuest, - .userCancelled, + .hostedUISignOutError, .globalSignOutError: XCTAssertEqual( resolver.resolve( diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift index 9bee6c05b1..a67aa2522b 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/AuthorizationTests/AWSAuthFetchSignInSessionOperationTests.swift @@ -584,6 +584,59 @@ class AWSAuthFetchSignInSessionOperationTests: BaseAuthorizationTests { } } + /// Test fetch session with authorization in error state + /// + /// - Given: An auth plugin with signedOut state + /// - When: + /// - I invoke fetchAuthSession and mock notSignedIn for getTokens + /// - Then: + /// - I should get an a valid session with the following details: + /// - isSignedIn = false + /// - aws credentails = valid values + /// - identity id = valid values + /// - cognito tokens = signedOut + /// + func testFetchSessionWithAuthorizationInErrorState() async throws { + + let initialState = AuthState.configured( + AuthenticationState.signedOut(.testData), + AuthorizationState.error(.sessionError(.service(AuthError.unknown("error")), .noCredentials))) + + let getId: MockIdentity.MockGetIdResponse = { _ in + return .init(identityId: "mockIdentityId") + } + + let getCredentials: MockIdentity.MockGetCredentialsResponse = { _ in + let credentials = CognitoIdentityClientTypes.Credentials(accessKeyId: "accessKey", + expiration: Date(), + secretKey: "secret", + sessionToken: "session") + return .init(credentials: credentials, identityId: "responseIdentityID") + } + + let plugin = configurePluginWith(identityPool: { + MockIdentity(mockGetIdResponse: getId, + mockGetCredentialsResponse: getCredentials) }, + initialState: initialState) + + let session = try await plugin.fetchAuthSession(options: AuthFetchSessionRequest.Options()) + XCTAssertFalse(session.isSignedIn) + + let creds = try? (session as? AuthAWSCredentialsProvider)?.getAWSCredentials().get() + XCTAssertNotNil(creds?.accessKeyId) + XCTAssertNotNil(creds?.secretAccessKey) + + let identityId = try? (session as? AuthCognitoIdentityProvider)?.getIdentityId().get() + XCTAssertNotNil(identityId) + + let tokensResult = (session as? AuthCognitoTokensProvider)?.getCognitoTokens() + guard case .failure(let error) = tokensResult, + case .signedOut = error else { + XCTFail("Should return signed out error") + return + } + } + /// Test signedOut state credential refresh /// /// - Given: Given an auth plugin with signedOut state and expired AWS credentials diff --git a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/AWSAuthSignOutTaskTests.swift b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/AWSAuthSignOutTaskTests.swift index 094e68c275..e940246c43 100644 --- a/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/AWSAuthSignOutTaskTests.swift +++ b/AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/TaskTests/ClientBehaviorTests/AWSAuthSignOutTaskTests.swift @@ -94,6 +94,26 @@ class AWSAuthSignOutTaskTests: BasePluginTest { } + func testInvalidStateForSignOutWhenSignedInUsingHostedUI() async { + + let initialState = AuthState.configured( + AuthenticationState.signedIn(.hostedUISignInData), + AuthorizationState.sessionEstablished(.hostedUITestData)) + + let authPlugin = configureCustomPluginWith(initialState: initialState) + + let result = await authPlugin.signOut() as? AWSCognitoSignOutResult + + guard case .failed(let authError) = result else { + XCTFail("Sign out should have failed.") + return + } + guard case .configuration = authError else { + XCTFail("Auth error should be service but got: \(authError)") + return + } + } + func testGuestSignOut() async { let initialState = AuthState.configured( diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift index 2e4c075b2c..7b78a2dc47 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift @@ -229,7 +229,7 @@ extension Model { let defaultFieldName = modelName.camelCased() + modelField.name.pascalCased() + "Id" if case let .belongsTo(_, targetNames) = modelField.association, !targetNames.isEmpty { return targetNames - } else if case let .hasOne(_, targetNames) = modelField.association, + } else if case let .hasOne(_, _, targetNames) = modelField.association, !targetNames.isEmpty { return targetNames } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift index a8aca2529e..f2e3a6f816 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/Support/QueryPredicate+GraphQL.swift @@ -131,7 +131,7 @@ extension QueryPredicateOperation: GraphQLFilterConvertible { } let targetName = targetNames.first ?? defaultFieldName return targetName - case .hasOne(_, let targetNames): + case .hasOne(_, _, let targetNames): guard targetNames.count == 1 else { preconditionFailure("QueryPredicate not supported on associated field with composite key: \(field)") } diff --git a/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift index 6ef7f146ed..7ac7e0faed 100644 --- a/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift @@ -15,7 +15,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.35.3" + public static let amplifyVersion = "2.35.7" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" diff --git a/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy b/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy index 74f8af8564..223288b3a4 100644 --- a/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy +++ b/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy @@ -1,8 +1,10 @@ - - NSPrivacyAccessedAPITypes - - + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift index 2cee4cccc6..ab3bf52aff 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/ModelSchema+SQLite.swift @@ -82,7 +82,7 @@ extension ModelField: SQLColumn { var sqlName: String { if case let .belongsTo(_, targetNames) = association { return foreignKeySqlName(withAssociationTargets: targetNames) - } else if case let .hasOne(_, targetNames) = association { + } else if case let .hasOne(_, _, targetNames) = association { return foreignKeySqlName(withAssociationTargets: targetNames) } return name diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift index 6045206d6d..4345db1a74 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/SQLite/Statement+Model.swift @@ -203,7 +203,7 @@ extension Statement: StatementModelConvertible { private func getTargetNames(field: ModelField) -> [String] { switch field.association { - case let .some(.hasOne(_, targetNames)): + case let .some(.hasOne(_, _, targetNames)): return targetNames case let .some(.belongsTo(_, targetNames)): return targetNames diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreObserveQueryTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreObserveQueryTests.swift index 4e13e3e2ff..fe67f60806 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreObserveQueryTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreObserveQueryTests.swift @@ -83,6 +83,7 @@ class DataStoreObserveQueryTests: SyncEngineIntegrationTestBase { try await clearDataStore() var snapshots = [DataStoreQuerySnapshot]() let snapshotWithIsSynced = expectation(description: "query snapshot with isSynced true") + snapshotWithIsSynced.assertForOverFulfill = false Amplify.Publisher.create(Amplify.DataStore.observeQuery(for: Post.self)).sink { completed in switch completed { case .finished: @@ -130,6 +131,7 @@ class DataStoreObserveQueryTests: SyncEngineIntegrationTestBase { var isObserveQueryReadyForTest = false let observeQueryReadyForTest = expectation(description: "received query snapshot with .isSynced true") let snapshotWithPost = expectation(description: "received first snapshot") + snapshotWithPost.assertForOverFulfill = false let post = Post(title: "title", content: "content", createdAt: .now()) Amplify.Publisher.create(Amplify.DataStore.observeQuery(for: Post.self)).sink { completed in switch completed { @@ -339,6 +341,7 @@ class DataStoreObserveQueryTests: SyncEngineIntegrationTestBase { try await clearDataStore() let snapshotWithIsSynced = expectation(description: "query snapshot with isSynced true") + snapshotWithIsSynced.assertForOverFulfill = false var snapshots = [DataStoreQuerySnapshot]() Amplify.Publisher.create(Amplify.DataStore.observeQuery(for: Post.self)).sink { completed in @@ -363,7 +366,6 @@ class DataStoreObserveQueryTests: SyncEngineIntegrationTestBase { await fulfillment(of: [snapshotWithIsSynced], timeout: 30) XCTAssertTrue(snapshots.count >= 2) XCTAssertFalse(snapshots[0].isSynced) - XCTAssertEqual(1, snapshots.filter({ $0.isSynced }).count) let theSyncedSnapshot = snapshots.first(where: { $0.isSynced }) XCTAssertNotNil(theSyncedSnapshot) diff --git a/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift b/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift index 35eb70b5b1..fab9543e07 100644 --- a/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift +++ b/AmplifyTests/CategoryTests/DataStore/ModelFieldAssociationTests.swift @@ -37,24 +37,37 @@ class ModelFieldAssociationTests: XCTestCase { func testHasOneWithCodingKeys() { let hasOne = ModelAssociation.hasOne(associatedWith: Comment.keys.post, targetNames: []) - guard case .hasOne(let fieldName, let target) = hasOne else { + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { XCTFail("Should create hasOne association") return } XCTAssertEqual(fieldName, Comment.keys.post.stringValue) + XCTAssertEqual(fieldNames, []) XCTAssertEqual(target, []) } func testHasOneWithCodingKeysWithTargetName() { let hasOne = ModelAssociation.hasOne(associatedWith: Comment.keys.post, targetNames: ["postID"]) - guard case .hasOne(let fieldName, let target) = hasOne else { + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { XCTFail("Should create hasOne association") return } XCTAssertEqual(fieldName, Comment.keys.post.stringValue) + XCTAssertEqual(fieldNames, []) XCTAssertEqual(target, ["postID"]) } + func testHasOneWithCodingKeysWithAssociatedFields() { + let hasOne = ModelAssociation.hasOne(associatedFields: [Comment.keys.post]) + guard case .hasOne(let fieldName, let fieldNames, let target) = hasOne else { + XCTFail("Should create hasOne association") + return + } + XCTAssertEqual(fieldName, nil) + XCTAssertEqual(fieldNames, ["post"]) + XCTAssertEqual(target, []) + } + func testBelongsToWithTargetName() { let belongsTo = ModelAssociation.belongsTo(targetName: "postID") guard case .belongsTo(let fieldName, let target) = belongsTo else { diff --git a/AmplifyTests/CoreTests/AmplifyPublisherTests.swift b/AmplifyTests/CoreTests/AmplifyPublisherTests.swift index dcdab6ed19..013cea0015 100644 --- a/AmplifyTests/CoreTests/AmplifyPublisherTests.swift +++ b/AmplifyTests/CoreTests/AmplifyPublisherTests.swift @@ -325,7 +325,7 @@ class AmplifyPublisherTests: XCTestCase { return nil } else { if fails && current > 4 { - throw Failure.unluckyNumber + throw AmplifyPublisherTests.Failure.unluckyNumber } else { return current } diff --git a/CHANGELOG.md b/CHANGELOG.md index 18819e9265..985f9ee10e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 2.35.7 (2024-07-16) + +### Bug Fixes + +- **core**: update privacy info (#3777) + +## 2.35.6 (2024-07-16) + +### Bug Fixes + +- **data**: Gen2 hasOne associatedFields compile issue (#3700) + +## 2.35.5 (2024-07-08) + +### Bug Fixes + +- **Auth**: Throw error if hosted UI is not presented during sign out (#3769) + +## 2.35.4 (2024-06-25) + +### Bug Fixes + +- **Auth**: Resolve AuthZ state correctly when in error state (#3762) +- **core**: remove InternalAmplifyCredentials from products (#3750) + ## 2.35.3 (2024-06-13) ### Bug Fixes diff --git a/Package.resolved b/Package.resolved index 9e4cd5bcac..156b876c86 100644 --- a/Package.resolved +++ b/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", - "version" : "1.5.4" + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" } } ], diff --git a/Package.swift b/Package.swift index 0fe21039e6..3a679faade 100644 --- a/Package.swift +++ b/Package.swift @@ -484,10 +484,6 @@ let package = Package( name: "AWSPluginsCore", targets: ["AWSPluginsCore"] ), - .library( - name: "InternalAmplifyCredentials", - targets: ["InternalAmplifyCredentials"] - ), .library( name: "AWSAPIPlugin", targets: ["AWSAPIPlugin"] diff --git a/README.md b/README.md index 3f766dc3bf..bb77a62dbf 100644 --- a/README.md +++ b/README.md @@ -56,21 +56,17 @@ of the library. Applications that evaluate all members of an enumeration using a `switch` statement can add a `default` case to prevent new cases from causing compile warnings or errors. +#### Semantic versioning and dependencies update + +We follow [semantic versioning for updating our dependencies](https://semver.org/#what-should-i-do-if-i-update-my-own-dependencies-without-changing-the-public-api). + ## License This library is licensed under the Apache 2.0 License. ## Installation -Amplify requires the following Xcode versions, according to the targeted platform: - -| Platform | Xcode Version | -| -------------:| ------------: | -| iOS | 15.0+ | -| macOS | 15.0+ | -| tvOS | 15.0+ | -| watchOS | 15.0+ | -| visionOS | 15 beta 2+ | +Amplify requires Xcode 15.0 or later for all the supported platforms. | For more detailed instructions, follow the getting started guides in our [documentation site](https://docs.amplify.aws/lib/q/platform/ios) | |-------------------------------------------------|