feat: README 多アカウント認証セクション追加

This commit is contained in:
joe 2026-03-06 13:04:08 +09:00
parent 1a89559648
commit e2ec67465e
2 changed files with 289 additions and 36 deletions

View file

@ -1,39 +1,38 @@
// Version: 1.0.0 // Version: 1.0.0
import 'dart:async'; import 'package:flutter/foundation.dart';
import 'dart:convert'; import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'package:googleapis_auth/google_auth.dart'; import 'package:googleapis_auth/google_auth.dart';
import 'package:googleapis/gmail/v1.dart'; import 'package:googleapis/gmail/v1.dart';
/// Gmail API /// Gmail API
/// ///
/// P2P /// P2P
/// BCC Gmail 使 /// BCC Gmail 使
class GmailWrapper { class GmailWrapper {
final String _gmailAddress; // BCC gmail final String _gmailAddress; // BCC gmail
final GAuthClient? _authClient; // OAuth GAuthClient? _authClient; // OAuth
final GmailService? _gmail; final GmailService? _gmail; // Gmail API
final GoogleSignIn _signIn; // GoogleSignIn
/// ///
factory GmailWrapper({ factory GmailWrapper({
required String gmailAddress, required String gmailAddress,
bool useOAuth = true, bool useOAuth = true,
}) async { }) {
if (useOAuth) { if (useOAuth) {
try { print('[Gmail] OAuth 認証モード。GoogleSignIn でアカウント選択を行います');
final client = GAuthClient.fromFile('credentials.json'); final signIn = GoogleSignIn(
return GmailWrapper._internal( scopes: [
gmailAddress: gmailAddress, 'https://www.googleapis.com/auth/gmail.readonly',
authClient: client, 'https://www.googleapis.com/auth/calendar.readonly',
); 'https://www.googleapis.com/auth/drive.readonly',
} catch (_) { 'https://www.googleapis.com/auth/spreadsheets.readonly',
// credentials.json null ],
print('[Gmail] credentials.json 未見。認証画面から開始してください'); );
return GmailWrapper._internal( return GmailWrapper._internal(
gmailAddress: gmailAddress, gmailAddress: gmailAddress,
authClient: null, signIn: signIn,
); );
}
} else { } else {
throw UnsupportedError('OAuth 方式でのみサポートされています'); throw UnsupportedError('OAuth 方式でのみサポートされています');
} }
@ -41,15 +40,25 @@ class GmailWrapper {
GmailWrapper._internal({ GmailWrapper._internal({
required this._gmailAddress, required this._gmailAddress,
GAuthClient? authClient, required GoogleSignIn signIn,
}) : _authClient = authClient, }) : _signIn = signIn,
_gmail = authClient != null ? GmailService(authClient) : null; _authClient = null,
_gmail = null;
/// IDBCC /// IDBCC
String get gmailAddress => _gmailAddress; String get gmailAddress => _gmailAddress;
/// ///
bool get isAuthorized => _authClient != null && _gmail != null; bool get isAuthorized => _gmail != null;
/// GoogleSignIn
GoogleSignIn get signInInstance => _signIn;
///
String? get currentAccountEmail => _signIn.currentUser?.email;
/// Google null
GoogleSignInAccount? get currentUser => _signIn.currentUser;
/// ///
Future<void> sendMessage({ Future<void> sendMessage({
@ -60,9 +69,6 @@ class GmailWrapper {
if (_gmail == null) return; if (_gmail == null) return;
try { try {
// HTML escaping
final body = base64Encode(utf8.encode(message));
final msg = EmailMessage( final msg = EmailMessage(
subject: '[SalesAssist1] チャット:$fromNode', subject: '[SalesAssist1] チャット:$fromNode',
to: toNode, to: toNode,
@ -89,15 +95,82 @@ class GmailWrapper {
return []; // return []; //
} }
/// /// OAuth
Future<GAuthClient?> authenticate() async { Future<GoogleSignInAccount?> authenticate() async {
try { try {
final client = GAuthClient.fromFile('credentials.json'); final account = await _signIn.signIn();
print('[Gmail] 認証済みクライアント使用'); if (account != null) {
return client; print('[Gmail] 認証成功:${account.email}');
return account;
} else {
throw Exception('ユーザーが認証をキャンセルしました');
}
} catch (e) { } catch (e) {
print('[Gmail] 認証フロー起動:$e'); print('[Gmail] 認証失敗:$e');
// GoogleSignIn rethrow;
}
}
///
Future<void> logout() async {
try {
await _signIn.signOut();
print('[Gmail] ログアウト済み');
} catch (e) {
print('[Gmail] ログアウト失敗:$e');
}
}
///
Future<GoogleSignInAccount?> switchAccount() async {
try {
final account = await _signIn.signIn();
if (account != null) {
print('[Gmail] アカウント切り替え:${account.email}');
return account;
} else {
throw Exception('ユーザーが認証をキャンセルしました');
}
} catch (e) {
print('[Gmail] 切り替え失敗:$e');
rethrow;
}
}
/// Gmail API
Future<void> initializeApi() async {
if (_authClient == null || _gmail != null) return;
try {
// credentials.json OAuth2
if (await kIsWeb || defaultTargetPlatform == Android) {
// Android credentials.json 使GoogleSignIn 使
print('[Gmail] GoogleSignIn のアクセストークンを使用');
// GAuthClient GmailService
final client = GAuthClient.withCredentials(
'google_sign_in_credentials.json', // SDK
);
_authClient = client;
_gmail = GmailService(client);
} else {
print('[Gmail] Web/iOS モード。GoogleSignIn で管理');
}
} catch (e) {
print('[Gmail] API 初期化失敗:$e');
rethrow;
}
}
/// GoogleSignIn
Future<String?> getToken() async {
if (_signIn.currentUser == null) return null;
try {
final auth = await _signIn.authenticator;
return auth.accessToken.toString();
} catch (e) {
print('[Gmail] トークン取得失敗:$e');
return null; return null;
} }
} }

View file

@ -0,0 +1,180 @@
// Version: 1.0.0
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
/// GoogleSignIn
class GoogleSignInProvider {
final List<GoogleSignIn> _accounts = [];
String? _currentAccountEmail;
/// 1
factory GoogleSignInProvider() {
return GoogleSignInProvider._();
}
GoogleSignInProvider._() {
//
final signIn = GoogleSignIn(
scopes: [
'email',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/spreadsheets.readonly',
],
);
_accounts.add(signIn);
}
///
String? get currentAccountEmail => _currentAccountEmail;
///
List<String> get allAccounts {
return _accounts
.where((s) => s.currentUser?.email != null)
.map((s) => '${s.currentUser?.displayName ?? '未認証'} (${s.currentUser?.email})')
.toList();
}
///
Future<GoogleSignInAccount?> login({String? accountEmail}) async {
try {
final signIn = _accounts.firstWhere(
(s) => s.currentUser?.email == null,
orElse: () => _accounts.last,
);
await signIn.signIn();
if (signIn.currentUser != null) {
_currentAccountEmail = signIn.currentUser!.email;
print('[GoogleSignIn] 認証済み:${signIn.currentUser!.email}');
return signIn.currentUser;
} else {
throw Exception('キャンセルまたはエラー');
}
} catch (e) {
print('[GoogleSignIn] 認証失敗:$e');
rethrow;
}
}
///
Future<void> logout() async {
final signIn = _accounts.firstWhere(
(s) => s.currentUser?.email == _currentAccountEmail,
orElse: () => _accounts.last,
);
await signOut();
_currentAccountEmail = null;
print('[GoogleSignIn] ログアウト');
}
///
Future<GoogleSignInAccount?> switchAccount({required String email}) async {
try {
final signIn = GoogleSignIn(
scopes: [
'email',
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/calendar.readonly',
'https://www.googleapis.com/auth/drive.readonly',
'https://www.googleapis.com/auth/spreadsheets.readonly',
],
);
final account = await signIn.signIn();
if (account?.email == email) {
_currentAccountEmail = email;
print('[GoogleSignIn] アカウント切り替え:$email');
return account;
} else {
throw Exception('メールアドレスが一致しません');
}
} catch (e) {
print('[GoogleSignIn] 切り替え失敗:$e');
rethrow;
}
}
/// Stream 使
Stream<GoogleSignInAccount?> get onAccountChanged =>
_accounts.firstWhere((s) => s.currentUser != null).authStateChanges;
}
///
class GoogleAccountsSelectScreen extends StatelessWidget {
final String? title;
final FutureOr<String>? selectedAccountEmail;
const GoogleAccountsSelectScreen({
super.key,
this.title,
this.selectedAccountEmail,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title ?? 'Google アカウント選択')),
body: _buildAccountList(context),
);
}
Widget _buildAccountList(BuildContext context) {
final signIn = GoogleSignIn();
final currentUser = signIn.currentUser;
final accounts = currentUser == null ? [] : [currentUser];
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'このアプリには複数の Google アカウントを登録できます。',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 16),
if (currentUser != null) ...[
Card(
child: ListTile(
leading: Icon(Icons.person, size: 32),
title: Text(currentUser.displayName ?? '未認証'),
subtitle: Text(currentUser.email),
isThreeLine: true,
onTap: () => _navigateToSettings(context),
),
),
],
const SizedBox(height: 8),
TextButton.icon(
onPressed: currentUser != null ? () => _navigateToSettings(context) : null,
icon: Icon(Icons.add_account),
label: Text('新規アカウントを追加'),
),
],
),
);
}
Future<void> _navigateToSettings(BuildContext context) async {
// GoogleSignin signOut()
final signIn = GoogleSignIn();
await signIn.signOut();
final auth = await GoogleSignInAuthentication(
scopes: [
'email',
'https://www.googleapis.com/auth/gmail.readonly',
],
);
final account = await signIn.signIn();
if (account != null) {
//
Navigator.of(context).pop(account?.email);
} else {
//
}
}
}