Tutorial · 20 min · advanced

Sign in with Apple & Google

The real ASAuthorizationController sheet and Credential Manager flow, from drop-in Dart APIs over FFI — plus the config each provider needs.

What you'll build — running on device.

Auth is the feature users judge hardest when it’s fake — the real Apple sheet and the real Google account picker are instantly recognizable. dartnative_social_sign_in drives both platforms’ genuine surfaces: ASAuthorizationController (the system controller behind Apple’s sign-in sheet) for Apple, and GIDSignIn on iOS / Credential Manager on Android for Google. The Dart APIs are shaped after the Flutter packages you already know (sign_in_with_apple, google_sign_in), so this is also a porting story. You’ll build a screen with sign-in buttons for each provider — each built two ways, as the native Button widget and as a hand-rolled GestureDetector — and a result card under each that shows the status, the profile fields, and the ID token that came back. The screen (SocialSignInDemo) is a byte-identical copy of the DartNative playground’s social sign-in demo.

What you need

  • A project from Your first DartNative app
  • dartnative_social_sign_in: ^1.0.0 in your pubspec
  • Provider configuration (Step 1 — the real work)

Step 1 — Configure the providers

The Dart code in this tutorial is short because the hard part of social sign-in isn’t code — it’s proving to each provider that your app is yours. That proof lives in project configuration, not in Dart:

Apple (iOS only): the tutorial project ships the Sign in with Apple capability pre-wired (Runner.entitlements, referenced from the target’s CODE_SIGN_ENTITLEMENTS). In your own app add it in Xcode: Runner target → Signing & Capabilities → + Capability → Sign in with Apple. Without it, the request fails with error 1000. The capability needs an explicit App ID — keep your bundle identifier unique to your team.

Google on iOS: add GoogleService-Info.plist to the Runner and register its REVERSED_CLIENT_ID as a URL scheme — the plugin reads the client ID from the plist’s CLIENT_ID key automatically. Those two keys only appear in the plist once the iOS OAuth client exists: enable the Google provider under Firebase Authentication and re-download the plist. Without them, signIn() completes with an error result telling you what’s missing.

Google on Android: Credential Manager needs an OAuth 2.0 client ID — the identifier Google issued when you registered the app in the Cloud console — and specifically your web one, not the Android one: the ID token it mints is addressed to your server, which is what will verify it. The tutorial reads it from a compile-time define, so no credential is ever committed:

dn run --dart-define=GOOGLE_SERVER_CLIENT_ID=xxxx.apps.googleusercontent.com

One more registration: Google only issues credentials to apps it recognizes, so an Android OAuth client with your package name and SHA-1 signing fingerprint must exist alongside the web one (./gradlew signingReport prints the debug SHA-1; in Firebase it’s Project settings → your Android app → Add fingerprint). Missing it is the classic “no credentials available” failure.

The screen doesn’t crash without any of this — the buttons still show, and auth errors land in the result card, so you can run it first and configure after.

Step 2 — Sign in with Apple

One awaited call presents the system sheet — Face ID and all. scopes is you telling Apple which profile fields to ask the user to share:

final credential = await SignInWithApple.getAppleIDCredential(
  scopes: [
    AppleIDAuthorizationScopes.email,
    AppleIDAuthorizationScopes.fullName,
  ],
);

When the future completes, the credential holds everything the screen displays:

setState(() {
  _appleStatus = 'Signed in ✓';
  _appleIdToken = _truncate(credential.identityToken);
  _appleGivenName = credential.givenName;
  _appleEmail = credential.email;
});

The identityToken is what your backend verifies before creating a session — the other fields are for showing the user who they are.

Two things every Apple integration must know:

  • Name and email arrive only on the first authorization. Persist them immediately; later sign-ins return null for both. (You can watch this happen in the tutorial — sign in twice and the Name/Email rows vanish from the result card.)
  • Cancellation is an exception, not a null. The demo catches it and turns it into a status line instead of an error:
} on SignInWithAppleAuthorizationException catch (e) {
  if (!mounted) return;
  setState(() {
    _appleStatus = e.code == AuthorizationErrorCode.canceled
        ? 'Canceled by user'
        : 'Auth error: ${e.message}';
  });
}

The method also guards Platform.isIOS first — Sign in with Apple is an iOS surface, and the demo says so inline rather than failing.

Step 3 — Sign in with Google

Google’s shape is the familiar google_sign_in one — construct, await signIn(), and mind the null:

// Android: serverClientId (web OAuth 2.0 client ID) is required by
// Credential Manager. Pass it via:
//   dn run --dart-define=GOOGLE_SERVER_CLIENT_ID=xxxx.apps.googleusercontent.com
// iOS: reads clientId from GoogleService-Info.plist automatically.
const googleSignIn = GoogleSignIn(
  serverClientId: String.fromEnvironment('GOOGLE_SERVER_CLIENT_ID'),
);
final account = await googleSignIn.signIn();
if (!mounted) return;
if (account == null) {
  setState(() => _googleStatus = 'Canceled by user');
  return;
}

Note the asymmetry — Google returns null on cancel, Apple throws — the tutorial handles both idioms side by side. A successful account carries the profile directly, and the ID token one level down:

setState(() {
  _googleStatus = 'Signed in ✓';
  _googleIdToken = _truncate(account.authentication.idToken);
  _googleDisplayName = account.displayName;
  _googleEmail = account.email;
});

serverClientId is required by Android’s Credential Manager and ignored where it isn’t needed — on iOS the client ID comes from GoogleService-Info.plist, so the same Dart code runs on both platforms.

Step 4 — Buttons and the result card

The screen shows each provider’s button twice, on purpose. The first variant (_NativeSignInButton) is the native Button widget — a real UIButton / MaterialButton — disabled while its request is in flight:

Button(
  title: label,
  imageAsset: assetIcon,
  foregroundColor: Colors.white,
  color: const Color(0xFF0169FF),
  shape: const StadiumBorder(),
  height: 44,
  fontSize: 15,
  fontWeight: FontWeight.bold,
  imageSize: 23,
  onPressed: isLoading ? null : onTap,
);

imageAsset puts the provider’s brand mark on the button — the tutorial bundles assets/apple-logo.png and assets/google-logo.png, and both companies publish strict button guidelines their review teams check. The second variant (_SignInButton) builds the same pill by hand from a GestureDetector + Container + Image row — proof that composed widgets and the native control land on the same look, and a template if you need a layout Button doesn’t offer.

Under the buttons, a _ResultCard renders the status line plus whatever identity fields actually came back, using collection-if so absent fields simply have no row:

_ResultCard(
  status: _googleStatus,
  rows: {
    if (_googleDisplayName != null) 'Name:': _googleDisplayName!,
    if (_googleEmail != null) 'Email:': _googleEmail!,
    if (_googleIdToken != null) 'ID token:': _googleIdToken!,
  },
),

About that ID token: it’s a signed JWT asserting “this provider authenticated this user” — the one artifact your backend should verify before creating a session. Tokens are hundreds of characters, so the demo shows just the first 24 (_truncate) to prove one arrived; a real app sends the full string to its server instead of displaying it.

Why this is native

The surfaces users authenticate through are the platform’s own — the Apple sheet with Face ID, the Google account picker with the device’s accounts — invoked over direct FFI with no MethodChannel in between. And because the Dart APIs mirror sign_in_with_apple and google_sign_in, an existing Flutter auth flow ports by swapping imports and keeping your backend verification unchanged.

The finished code

In the public repo — dn create ., dn run. The screen at lib/screens/social_sign_in_demo.dart is a byte-identical copy of the playground’s social sign-in demo (lib/screens/home/demo_ui.dart is the playground’s shared UI kit — the kHomeBg / kTextPrimary palette the screen paints with — also verbatim, and the logos under assets/ come from the playground too); only the thin lib/main.dart that registers the plugins and runs SocialSignInDemo is tutorial-specific. When the playground screen improves, this tutorial inherits it by copying the files again.

Open the finished code →