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.0in 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
nullfor 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_appleandgoogle_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.