gmail_client_flutter
Flutter bindings for gmail_client. Provides Riverpod providers, Google Sign-In integration, and platform-specific OAuth utilities.
Features
- Riverpod providers for auth state, inbox, compose, and email detail
- GoogleAuthService — Google Sign-In and Gmail OAuth token exchange (web + mobile)
- Platform-specific OAuth — web popup flow via
gmail_oauth - URL utils — clean OAuth params from browser address bar
- Attachment management — file picking and MIME type mapping
Prerequisites
gmail_client added as dependencySupabase project initialized withsupabase_flutterDatabase tables created (see gmail_client docs)Edge Functions deployed (google-auth-callback,send-email,list-emails,get-email)Google Cloud OAuth credentials configured.envregistered as asset inpubspec.yaml(if usingflutter_dotenv)
Getting started
flutter pub add gmail_client_flutter
1. Register .env as an asset (if using flutter_dotenv)
# pubspec.yaml
flutter:
uses-material-design: true
assets:
- .env
2. Initialize in main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:gmail_client_flutter/gmail_client_flutter.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load();
await Supabase.initialize(
url: dotenv.env['SUPABASE_URL']!,
anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
);
runApp(const ProviderScope(child: MyApp()));
}
3. Use providers in your widgets
class InboxPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final inboxState = ref.watch(inboxProvider);
if (inboxState.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: inboxState.emails.length,
itemBuilder: (context, index) {
final email = inboxState.emails[index];
return ListTile(
title: Text(email.subject ?? ''),
subtitle: Text(email.from ?? ''),
);
},
);
}
}
Security
Do not commit client_secret or .env to your repository. The .env file is listed in .gitignore. Only .env.example should be committed with placeholder values.
The Google OAuth client secret is stored server-side in org_email_config and never exposed to the Flutter client. Use .env only for non-sensitive values like the Supabase URL and anon key.
Providers
| Provider | Type | Description |
|---|---|---|
emailServiceProvider |
Provider<EmailService> |
Core email service instance |
supabaseClientProvider |
Provider<SupabaseClient> |
Supabase client singleton |
emailConnectionProvider |
FutureProvider<bool> |
Whether Gmail is connected |
connectedEmailProvider |
FutureProvider<String?> |
Connected email address |
displayNameProvider |
FutureProvider<String?> |
Sender display name |
authStateProvider |
StreamProvider<AuthState> |
Supabase auth state stream |
currentUserProvider |
Provider<User?> |
Current authenticated user |
isLoggedInProvider |
Provider<bool> |
Whether session is active |
inboxProvider |
StateNotifierProvider |
Inbox state (emails, loading, pagination) |
composeProvider |
StateNotifierProvider |
Compose state (attachments, sending) |
emailDetailProvider |
FutureProvider.family |
Email detail by message ID |
googleAuthServiceProvider |
Provider<GoogleAuthService> |
Google Sign-In service |
Web vs Mobile OAuth
The OAuth flow differs by platform. Use the appropriate method:
Web (popup)
final googleAuth = ref.read(googleAuthServiceProvider);
// Opens a popup window for Gmail OAuth consent
final code = await googleAuth.getGmailAuthCodeForWeb('YOUR_WEB_CLIENT_ID');
if (code != null) {
await googleAuth.connectGmailAccount(
serverAuthCode: code,
email: 'user@gmail.com',
userId: supabaseUser.id,
);
}
Mobile (iOS/Android — native Google Sign-In)
final googleAuth = ref.read(googleAuthServiceProvider);
await googleAuth.initialize(
webClientId: 'xxx.apps.googleusercontent.com',
iosClientId: 'xxx.apps.googleusercontent.com',
);
final account = await googleAuth.signIn();
final code = await googleAuth.getServerAuthCode(account!);
if (code != null) {
await googleAuth.connectGmailAccount(
serverAuthCode: code,
email: account.email,
userId: supabaseUser.id,
);
}
GoogleAuthService
final googleAuth = ref.read(googleAuthServiceProvider);
// Initialize with OAuth client IDs
await googleAuth.initialize(
webClientId: 'xxx.apps.googleusercontent.com',
iosClientId: 'xxx.apps.googleusercontent.com',
);
// Sign in (mobile)
final account = await googleAuth.signIn();
// Get OAuth code (mobile)
final code = await googleAuth.getServerAuthCode(account!);
// Get OAuth code via web popup
final code = await googleAuth.getGmailAuthCodeForWeb('web-client-id');
// Exchange code for Gmail tokens
await googleAuth.connectGmailAccount(
serverAuthCode: code!,
email: 'user@gmail.com',
userId: supabaseUser.id,
);
Complete Flow Example
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:gmail_client_flutter/gmail_client_flutter.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Load environment variables
await dotenv.load();
// 2. Initialize Supabase
await Supabase.initialize(
url: dotenv.env['SUPABASE_URL']!,
anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
);
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Consumer(
builder: (context, ref, _) {
final isLoggedIn = ref.watch(isLoggedInProvider);
return isLoggedIn ? const HomePage() : const LoginPage();
},
),
);
}
}
class LoginPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
// Sign in with Google (Supabase Auth)
await Supabase.instance.client.auth.signInWithOAuth(
OAuthProvider.google,
);
},
child: const Text('Sign in with Google'),
),
),
);
}
}
class HomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isConnected = ref.watch(emailConnectionProvider);
final inbox = ref.watch(inboxProvider);
final connectedEmail = ref.watch(connectedEmailProvider);
return Scaffold(
appBar: AppBar(
title: Text(connectedEmail.value ?? 'Gmail App'),
),
body: isConnected.when(
data: (connected) => connected
? ListView.builder(
itemCount: inbox.emails.length,
itemBuilder: (_, i) => ListTile(
title: Text(inbox.emails[i].subject ?? ''),
subtitle: Text(inbox.emails[i].from ?? ''),
),
)
: const Center(child: Text('Connect your Gmail account')),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Navigate to compose screen
Navigator.push(context,
MaterialPageRoute(builder: (_) => const ComposeScreen()));
},
child: const Icon(Icons.edit),
),
);
}
}
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
FileNotFoundError when loading .env |
.env not registered as asset in pubspec.yaml |
Add assets: - .env under flutter: in pubspec.yaml and restart |
AssertionError on SUPABASE_URL / SUPABASE_ANON_KEY |
.env variables are missing or misspelled |
Check .env has SUPABASE_URL= and SUPABASE_ANON_KEY= |
| CORS errors when calling Edge Functions from Chrome | localhost and supabase.co are different origins |
Run with: flutter run -d chrome --web-port=3000 --web-browser-flag=--disable-web-security |
| Google Sign-In not working on web | Missing or wrong webClientId |
Ensure GOOGLE_WEB_CLIENT_ID in .env matches Google Cloud Console |
inboxProvider always returns empty list |
Gmail not connected, or no session | Check isLoggedInProvider and emailConnectionProvider first |
GmailAuthException: User not authenticated |
Supabase session expired or missing | Sign in with Supabase.instance.client.auth.signInWithOAuth() |
GmailClientException: relation does not exist |
Database tables not created | Run supabase db push or create tables manually (see gmail_client docs) |
Provider returns null or loading forever |
ProviderScope not wrapping runApp |
Ensure runApp(const ProviderScope(child: MyApp())) |