Published on

How to Add Apple Sign In to Flutter Application

Authors
  • avatar
    Name
    Rosa Tiara
    Twitter

Introduction

If your Flutter app targets iOS users, you need to add Apple Sign In. The App Store requires it whenever your app includes third-party login options like Google or Facebook.

Why Apple Sign In?

  1. Users can choose to hide their email address
  2. Face ID/Touch ID integration for quick authentication
  3. Required if you offer other social sign-in options
  4. Works on iOS, macOS, watchOS, and tvOS

Prerequisites

Before we start, make sure you have:

  • Flutter SDK installed (2.0 or higher recommended)
  • An Apple Developer account (required for setting up capabilities)
  • Xcode installed (for iOS development)
  • Basic understanding of Flutter and Dart

Step 1: Add Dependencies

First, add the sign_in_with_apple package to your pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  sign_in_with_apple: ^7.0.1

Run flutter pub get to install the package.

Step 2: Configure Your Apple Developer Account

Enable Sign in with Apple Capability

  1. Go to Apple Developer Portal and sign in
  2. Navigate to Certificates, Identifiers & Profiles
  3. Select your app's Identifier
  4. Enable Sign in with Apple capability
  5. Click Save

Configure Your Xcode Project

  1. Open your Flutter project in Xcode:

    open ios/Runner.xcworkspace
    
  2. Select your project in the navigator

  3. Go to Signing & Capabilities tab

  4. Click + Capability

  5. Add Sign in with Apple

Your Xcode project is now configured!

Step 3: Basic Implementation

Let's create a simple Apple Sign In button and handle the authentication flow.

Create the Sign In Button

import 'package:flutter/material.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';

class AppleSignInButton extends StatelessWidget {
  const AppleSignInButton({Key? key}) : super(key: key);

  Future<void> _handleAppleSignIn() async {
    try {
      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [
          AppleIDAuthorizationScopes.email,
          AppleIDAuthorizationScopes.fullName,
        ],
      );

      // Successfully signed in
      print('User ID: ${credential.userIdentifier}');
      print('Email: ${credential.email}');
      print('Name: ${credential.givenName} ${credential.familyName}');
      
      // Handle the authentication with your backend here
      
    } catch (error) {
      print('Error during Apple Sign In: $error');
    }
  }

  
  Widget build(BuildContext context) {
    return SignInWithAppleButton(
      onPressed: _handleAppleSignIn,
    );
  }
}

Understanding the Credential Response

When a user successfully signs in, you will receive an AuthorizationCredentialAppleID object that contains:

  • userIdentifier - unique user ID (use this as the primary identifier)
  • email - user's email (may be a proxy email if they chose to hide their real email)
  • givenName - user's first name
  • familyName - user's last name
  • identityToken - JWT token for backend verification
  • authorizationCode - one-time use authorization code

Important: Name and email are only provided on the first sign-in, so it's best to store them immediately.

Step 4: Custom Styling

You can customize the Apple Sign In button to match your app's design:

SignInWithAppleButton(
  onPressed: _handleAppleSignIn,
  style: SignInWithAppleButtonStyle.black, // or .white, .whiteOutline
  borderRadius: BorderRadius.circular(8),
  iconAlignment: IconAlignment.center,
  height: 50,
  text: 'Sign in with Apple', // Custom text
)

Step 5: Backend Integration

For production apps, you should verify the identity token with your backend:

Future<void> _signInWithApple() async {
  try {
    final credential = await SignInWithApple.getAppleIDCredential(
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
    );

    // Send to your backend for verification
    final response = await _verifyWithBackend(
      identityToken: credential.identityToken,
      authorizationCode: credential.authorizationCode,
      userIdentifier: credential.userIdentifier,
    );

    // Handle successful authentication
    if (response.success) {
      // Navigate to home screen
      Navigator.pushReplacementNamed(context, '/home');
    }
    
  } catch (error) {
    // Handle error
    _showErrorDialog(error.toString());
  }
}

Future<AuthResponse> _verifyWithBackend({
  required String? identityToken,
  required String? authorizationCode,
  required String userIdentifier,
}) async {
  // Implement your backend verification here
  // Your backend should verify the identityToken with Apple's servers
  
  final response = await http.post(
    Uri.parse('https://your-api.com/auth/apple'),
    body: jsonEncode({
      'identity_token': identityToken,
      'authorization_code': authorizationCode,
      'user_identifier': userIdentifier,
    }),
  );
  
  return AuthResponse.fromJson(jsonDecode(response.body));
}

Step 6: Handle Sign Out

Don't forget to implement sign-out functionality:

Future<void> _signOut() async {
  // Clear your local session
  await _clearUserSession();
  
  // Navigate to login screen
  Navigator.pushReplacementNamed(context, '/login');
}

Note: Apple doesn't provide a sign-out API. You only need to clear your app's local session.

Step 7: Check Authentication Status

Check if a user is currently signed in when your app starts:

class AuthService {
  // Store user ID in secure storage
  Future<bool> isUserSignedIn() async {
    final userId = await _secureStorage.read(key: 'user_id');
    return userId != null;
  }

  // Get credential state
  Future<CredentialState> getCredentialState(String userIdentifier) async {
    final state = await SignInWithApple.getCredentialState(userIdentifier);
    return state;
  }
}

The credential state can be:

  • authorized - User is signed in
  • revoked - User revoked access
  • notFound - No credential found
  • transferred - Account was transferred

Best Practices

1. Store User Information Immediately

Apple only provides the user's name and email on the first sign-in. Store them right away:

Future<void> _storeUserInfo(AuthorizationCredentialAppleID credential) async {
  // Only available on first sign-in
  if (credential.givenName != null) {
    await _secureStorage.write(key: 'given_name', value: credential.givenName);
  }
  if (credential.familyName != null) {
    await _secureStorage.write(key: 'family_name', value: credential.familyName);
  }
  if (credential.email != null) {
    await _secureStorage.write(key: 'email', value: credential.email);
  }
  
  // Always available
  await _secureStorage.write(key: 'user_id', value: credential.userIdentifier);
}

2. Handle Revocation

Revocation = the act of withdrawing, cancelling, or invalidating a previously granted permission, right, access, or credential.

Listen for credential revocation and handle it:

void _checkCredentialState(String userIdentifier) async {
  final credentialState = await SignInWithApple.getCredentialState(userIdentifier);
  
  if (credentialState == CredentialState.revoked) {
    // User revoked access, sign them out
    await _signOut();
    _showMessage('Your Apple ID sign-in was revoked. Please sign in again.');
  }
}

3. Use Secure Storage

Always use secure storage (like flutter_secure_storage) for storing sensitive information:

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final _secureStorage = FlutterSecureStorage();

// Store
await _secureStorage.write(key: 'user_id', value: userId);

// Read
final userId = await _secureStorage.read(key: 'user_id');

// Delete
await _secureStorage.delete(key: 'user_id');

4. Error Handling

Implement clear error handling:

Future<void> _handleAppleSignIn() async {
  try {
    final credential = await SignInWithApple.getAppleIDCredential(
      scopes: [
        AppleIDAuthorizationScopes.email,
        AppleIDAuthorizationScopes.fullName,
      ],
    );
    
    await _processCredential(credential);
    
  } on SignInWithAppleAuthorizationException catch (e) {
    // Handle specific Apple Sign In errors
    switch (e.code) {
      case AuthorizationErrorCode.canceled:
        print('User canceled the sign-in');
        break;
      case AuthorizationErrorCode.failed:
        print('Authorization failed');
        break;
      case AuthorizationErrorCode.invalidResponse:
        print('Invalid response');
        break;
      case AuthorizationErrorCode.notHandled:
        print('Not handled');
        break;
      case AuthorizationErrorCode.unknown:
        print('Unknown error');
        break;
    }
  } catch (e) {
    // Handle other errors
    print('Unexpected error: $e');
  }
}

Testing

On Physical Device

Apple Sign In only works on physical devices, not on simulators. To test:

  1. Build and run on a physical iOS device
  2. Make sure your device is signed in to iCloud
  3. Test the sign-in flow

Test with Sandbox Account

For testing without affecting your production data:

  1. Go to SettingsApple IDPassword & SecurityApps Using Apple ID
  2. Create a sandbox account in App Store Connect
  3. Sign in with the sandbox account on your device

Common Issues and Solutions

Issue 1: "Invalid Client" Error

Solution: Make sure the Bundle ID in Xcode matches the one configured in Apple Developer Portal.

Issue 2: Button Not Showing

Solution: Check that:

  • You're testing on a physical device (not simulator)
  • Sign in with Apple capability is enabled in Xcode
  • Your app's identifier has the capability enabled in Developer Portal

Issue 3: Email or Name is Null

Solution: This is expected after the first sign-in. Apple only provides this information once. Make sure to store it on the first authentication.

Issue 4: "Unsupported" Error on Android

Solution: If you need cross-platform support, use the webAuthenticationOptions parameter:

final credential = await SignInWithApple.getAppleIDCredential(
  scopes: [
    AppleIDAuthorizationScopes.email,
    AppleIDAuthorizationScopes.fullName,
  ],
  webAuthenticationOptions: WebAuthenticationOptions(
    clientId: 'your.bundle.id',
    redirectUri: Uri.parse('https://your-redirect-uri.com/callback'),
  ),
);

Happy coding! 🚀