apple_sign_in_plugin_linux 0.0.1-dev.2
apple_sign_in_plugin_linux: ^0.0.1-dev.2 copied to clipboard
Linux implementation of the apple_sign_in_plugin package, using Apple's official web authorization flow via system browser and MethodChannel.
// Copyright 2026 DHC Tech
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.
import 'package:apple_sign_in_plugin_linux/apple_sign_in_plugin_linux.dart';
import 'package:apple_sign_in_plugin_platform_interface/apple_sign_in_plugin_platform_interface.dart';
import 'package:flutter/foundation.dart';
import 'package:material_ui/material_ui.dart';
void main() {
// Like Windows (and unlike Darwin, which has a native API), Linux has no
// native Apple Sign-In mechanism — it goes through Apple's web
// authorization flow via the system browser, so it needs your Apple
// Services ID and redirect URI set before signIn() can be called.
AppleSignInPlatform.instance = AppleSignInLinuxImpl()
..config = const AppleSignInDesktopConfig(
serviceId: 'com.example.app.service',
redirectUri: 'https://example.com/callbacks/apple',
callbackScheme: 'com.example.app',
);
runApp(const MyApp());
}
/// The example app's root widget.
class MyApp extends StatelessWidget {
/// Creates the example app.
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Apple Sign In Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.black),
useMaterial3: true,
),
home: const SignInPage(),
);
}
}
/// Page demonstrating the Apple Sign-In flow.
///
/// This app does not perform its own token exchange or storage — those are
/// backend responsibilities (see the
/// ["Backend Boundary"](https://pub.dev/packages/apple_sign_in_plugin_platform_interface#backend-boundary)
/// section). It only demonstrates requesting a credential from Apple and
/// displaying what was returned.
class SignInPage extends StatefulWidget {
/// Creates the sign-in page.
const SignInPage({super.key});
@override
State<SignInPage> createState() => _SignInPageState();
}
class _SignInPageState extends State<SignInPage> {
AppleCredential? _credential;
bool _isLoading = false;
String? _errorText;
/// Initiates the Apple Sign-In flow and displays the returned
/// [AppleCredential].
Future<void> _signIn() async {
setState(() {
_isLoading = true;
_errorText = null;
});
try {
final AppleCredential credential = await AppleSignInPlatform.instance.signIn(
scopes: {.email, .fullName},
);
setState(() => _credential = credential);
} on AppleSignInException catch (e) {
if (e.code == .canceled) {
// The user dismissed the sheet; nothing to report.
return;
}
if (kDebugMode) {
debugPrint('Sign in failed: $e');
}
setState(() => _errorText = e.message);
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
/// Clears the locally displayed credential.
///
/// This only resets this screen's UI state — it does not sign the user
/// out of their Apple ID, and it does not revoke anything with Apple.
/// A real app's backend owns its own session/sign-out.
void _clear() {
setState(() => _credential = null);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Apple Sign In Plugin'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: _isLoading
? const CircularProgressIndicator()
: Padding(
padding: const EdgeInsets.all(24.0),
child: _credential != null
? _buildCredentialView(_credential!)
: _buildLoginButton(),
),
),
);
}
Widget _buildLoginButton() {
return Column(
mainAxisAlignment: .center,
children: [
const Icon(Icons.apple, size: 80),
const SizedBox(height: 20),
const Text('Sign in to access your account', style: TextStyle(fontSize: 18)),
if (_errorText != null) ...[
const SizedBox(height: 16),
Text(_errorText!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: _signIn,
icon: const Icon(Icons.apple),
label: const Text('Sign in with Apple'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
),
),
],
);
}
Widget _buildCredentialView(AppleCredential credential) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: .center,
crossAxisAlignment: .start,
children: [
const Center(child: Icon(Icons.account_circle, size: 80)),
const SizedBox(height: 30),
const Text('Apple Credential:', style: TextStyle(fontSize: 20, fontWeight: .bold)),
const Divider(),
_infoRow('User ID', _truncated(credential.userIdentifier)),
_infoRow('Email', credential.email ?? 'Not shared this time'),
_infoRow(
'Name',
credential.name == null
? 'Not shared this time'
: '${credential.name!.givenName ?? ''} '
'${credential.name!.familyName ?? ''}'
.trim(),
),
_infoRow('Authorized scopes', credential.authorizedScopes.map((s) => s.name).join(', ')),
_infoRow('Real user status', credential.realUserStatus.name),
const Divider(),
const Text('Send to your backend:', style: TextStyle(fontSize: 16, fontWeight: .bold)),
const SizedBox(height: 10),
_secretRow('Identity token', credential.identityToken),
_secretRow('Authorization code', credential.authorizationCode),
const SizedBox(height: 40),
SizedBox(
width: double.infinity,
height: 50,
child: OutlinedButton.icon(
onPressed: _clear,
icon: const Icon(Icons.clear),
label: const Text('Clear (local UI only)'),
),
),
],
),
);
}
String _truncated(String value) => value.length > 12 ? '${value.substring(0, 12)}…' : value;
Widget _infoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: .start,
children: [
SizedBox(
width: 130,
child: Text('$label:', style: const TextStyle(fontWeight: .bold)),
),
Expanded(child: Text(value)),
],
),
);
}
Widget _secretRow(String label, String? secret) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Column(
crossAxisAlignment: .start,
children: [
Text('$label:', style: const TextStyle(fontWeight: .bold)),
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
child: Text(
// Never display the full secret — this app is not the
// consumer of these values, the backend is.
secret != null
? '${_maskedPreview(secret)} [sent to backend, not shown]'
: 'Not available',
style: const TextStyle(fontFamily: 'Courier', fontSize: 12),
),
),
],
),
);
}
}
// This example intentionally duplicates its UI/logic across each
// platform package's own example app (see the other apple_sign_in_plugin_*
// example/lib/main.dart files) rather than sharing a common package —
// each platform example is meant to be independently runnable, matching
// Flutter's own federated-plugin example convention.
/// Returns a short, masked preview of [secret] that never reveals the full
/// value — not even a single character of it — regardless of how short it
/// is.
String _maskedPreview(String secret) {
if (secret.isEmpty) {
return '';
}
const previewLength = 8;
// Secrets short enough that showing any of their characters (plus an
// ellipsis) could reveal, or nearly reveal, the whole value are fully
// masked instead.
if (secret.length <= 8) {
return '•' * previewLength;
}
final String start = secret.substring(0, 2);
final String end = secret.substring(secret.length - 2);
return '$start…$end';
}