Published on

How to Add Apple Sign In to Flutter Application

Authors
  • avatar
    Name
    Rosa Tiara Galuh
    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! 🚀