diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 8b02b81..22c87d0 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -6,40 +6,68 @@ on: pull_request: branches: [ main ] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: name: Build and Test SDKHostApp scheme using any available iPhone simulator - runs-on: macos-latest + # macos-26 is required: only this image's installed Xcodes include + # 26.4.1+, where the stricter protocol-witness availability check + # this PR exists to fix (issue #310) lives. macos-15 tops out at + # Xcode 26.3, which does not exercise the check and would let + # regressions of the @available annotations slip through. + runs-on: macos-26 + env: + SCHEME: SDKHostApp + PROJECT: IFTTT SDK.xcodeproj steps: - name: Checkout - uses: actions/checkout@v2 - - name: Build and Test - env: - scheme: ${{ 'SDKHostApp' }} + uses: actions/checkout@v4 + + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Toolchain versions run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - - if [ $scheme = default ]; then scheme=$(cat default); fi - - # Determine file to build: .xcworkspace or .xcodeproj - if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi - - # Clean up whitespace - file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` - - # Find first available simulator - device_name=$(xcrun simctl list devices available | grep "iPhone" | head -n 1 | sed -E 's/^[[:space:]]*([^()]+)[[:space:]]*\(.*$/\1/' | awk '{$1=$1; print}') - - if [ -z "$device_name" ]; then - echo "❌ Failed to find a valid iOS device." - exit 1 + xcodebuild -version + swift --version + + - name: Install xcbeautify + run: | + if ! command -v xcbeautify >/dev/null 2>&1; then + brew install xcbeautify fi - echo "📱 Using device: $device_name" + - name: Pick iPhone simulator + id: sim + run: | + udid=$(xcrun simctl list devices available --json \ + | jq -r ' + [ .devices + | to_entries[] + | select(.key | contains("iOS")) + | .value[] + | select((.name | startswith("iPhone")) and .isAvailable == true) + ] + | (.[0].udid // empty) + ') + if [ -z "$udid" ]; then + echo "No available iPhone simulator found" >&2 + exit 1 + fi + echo "destination=platform=iOS Simulator,id=$udid" >> "$GITHUB_OUTPUT" + echo "Using simulator $udid" - # Build and run the tests + - name: Build & test + run: | + set -o pipefail xcodebuild test \ - -scheme "$scheme" \ - -"$filetype_parameter" "$file_to_build" \ - -destination "platform=iOS Simulator,name=$device_name" + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -destination "${{ steps.sim.outputs.destination }}" \ + | xcbeautify --renderer github-actions diff --git a/IFTTT SDK/AuthenticationSessionPresentationContextProvider.swift b/IFTTT SDK/AuthenticationSessionPresentationContextProvider.swift index 91bcbf3..a8e42a2 100644 --- a/IFTTT SDK/AuthenticationSessionPresentationContextProvider.swift +++ b/IFTTT SDK/AuthenticationSessionPresentationContextProvider.swift @@ -7,11 +7,10 @@ import AuthenticationServices -/// A class that conforms to `ASWebAuthenticationPresentationContextProviding`. -class AuthenticationSessionContextPresentationProvider: NSObject, ASWebAuthenticationPresentationContextProviding, ASAuthorizationControllerPresentationContextProviding { +class AuthenticationSessionContextPresentationProvider: NSObject { /// The window context that the presentation of the authentication should take place in. private let presentationContext: UIWindow - + /// Creates an instance of `AuthenticationSessionContextProvider`. /// /// - Parameters: @@ -20,13 +19,17 @@ class AuthenticationSessionContextPresentationProvider: NSObject, ASWebAuthentic self.presentationContext = presentationContext super.init() } - - @available(iOS 12.0, *) +} + +@available(iOS 12.0, *) +extension AuthenticationSessionContextPresentationProvider: ASWebAuthenticationPresentationContextProviding { func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { return presentationContext } - - @available(iOS 13.0, *) +} + +@available(iOS 13.0, *) +extension AuthenticationSessionContextPresentationProvider: ASAuthorizationControllerPresentationContextProviding { func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return presentationContext } diff --git a/IFTTT SDK/ConnectButton.swift b/IFTTT SDK/ConnectButton.swift index 43892dc..21ece5f 100644 --- a/IFTTT SDK/ConnectButton.swift +++ b/IFTTT SDK/ConnectButton.swift @@ -345,9 +345,10 @@ public class ConnectButton: UIView { footerLabel.constrain.edges(to: footerLabelContainer, edges: [.left, .top, .right]) // But allow it to be shorter than its container - footerLabel.bottomAnchor.constraint(lessThanOrEqualTo: footerLabelContainer.bottomAnchor) + footerLabel.bottomAnchor.constraint(lessThanOrEqualTo: footerLabelContainer.bottomAnchor).isActive = true let breakableBottomConstraint = footerLabel.bottomAnchor.constraint(equalTo: footerLabelContainer.bottomAnchor) breakableBottomConstraint.priority = .defaultHigh + breakableBottomConstraint.isActive = true // Ask the label to keep its intrinsic height footerLabel.setContentHuggingPriority(.required, for: .vertical) diff --git a/IFTTT SDK/SignInWithAppleAuthentication.swift b/IFTTT SDK/SignInWithAppleAuthentication.swift index a004503..9a2bd16 100644 --- a/IFTTT SDK/SignInWithAppleAuthentication.swift +++ b/IFTTT SDK/SignInWithAppleAuthentication.swift @@ -40,21 +40,33 @@ final class AppleSignInWebService: ServiceAuthentication { @available(iOS 13.0, *) func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { - let authorizationError = ASAuthorizationError(_nsError: error as NSError) - switch authorizationError.code { - case .canceled: - completion(.failure(.userCanceled)) - case .failed: - completion(.failure(.failed)) - case .invalidResponse: - completion(.failure(.invalidResponse)) - case .notHandled: - completion(.failure(.notHandled)) - case .unknown: - completion(.failure(.unknown)) - @unknown default: - completion(.failure(.unknown)) + let code = ASAuthorizationError(_nsError: error as NSError).code + // If/else cascade rather than a switch: ASAuthorizationError.Code is + // non-frozen and gains new cases (notInteractive in 15.4, ...) that + // can only be referenced behind #available — a switch over them + // either trips "switch must be exhaustive" warnings or requires + // raising the deployment target. + let mapped: AuthenticationError + if code == .canceled { + mapped = .userCanceled + } else if code == .failed { + mapped = .failed + } else if code == .invalidResponse { + mapped = .invalidResponse + } else if code == .notHandled { + mapped = .notHandled + } else if #available(iOS 15.4, *), code == .notInteractive { + mapped = .notInteractive + } else if #available(iOS 18.0, *), code == .matchedExcludedCredential { + mapped = .matchedExcludedCredential + } else if #available(iOS 18.2, *), code == .credentialImport { + mapped = .credentialImport + } else if #available(iOS 18.2, *), code == .credentialExport { + mapped = .credentialExport + } else { + mapped = .unknown } + completion(.failure(mapped)) } } diff --git a/IFTTT SDK/WebServiceAuthentication.swift b/IFTTT SDK/WebServiceAuthentication.swift index c14863f..85a3025 100644 --- a/IFTTT SDK/WebServiceAuthentication.swift +++ b/IFTTT SDK/WebServiceAuthentication.swift @@ -14,13 +14,26 @@ import AuthenticationServices /// - invalidResponse: The response returned by the web service was invalid. /// - notHandled: The error wasn't handled by the web service. /// - presentationContextInvalid: The presentation content provided was invalid. -/// - unknown: Some unknown error ocurred when authenticating with the web service. +/// - notInteractive: The authorization request was performed in a +/// non-interactive context. Mirrors `ASAuthorizationError.Code.notInteractive` +/// (iOS 15.4+). +/// - matchedExcludedCredential: A matched credential was excluded from use. +/// Mirrors `ASAuthorizationError.Code.matchedExcludedCredential` (iOS 18+). +/// - credentialImport: An error occurred importing a credential. Mirrors +/// `ASAuthorizationError.Code.credentialImport` (iOS 18.2+). +/// - credentialExport: An error occurred exporting a credential. Mirrors +/// `ASAuthorizationError.Code.credentialExport` (iOS 18.2+). +/// - unknown: Some unknown error occurred when authenticating with the web service. enum AuthenticationError: Error { case userCanceled case failed case invalidResponse case notHandled case presentationContextInvalid + case notInteractive + case matchedExcludedCredential + case credentialImport + case credentialExport case unknown }