bapp_auth 0.2.1
bapp_auth: ^0.2.1 copied to clipboard
Cross-platform authentication for BAPP framework platforms with Keycloak SSO and automatic token management
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:bapp_auth/bapp_auth.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bapp Auth Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final _clientIdController = TextEditingController(text: 'bapp-pos');
final _hostController =
TextEditingController(text: 'https://panel.bapp.ro/api');
BappAuth? _auth;
String _status = 'Not initialized';
String _apiResponse = '';
@override
void dispose() {
_clientIdController.dispose();
_hostController.dispose();
_auth?.dispose();
super.dispose();
}
BappAuth _getAuth() {
_auth?.dispose();
_auth = BappAuth(
config: BappAuthConfig(
clientId: _clientIdController.text,
host: _hostController.text,
app: 'erp',
),
);
_auth!.authStateStream.listen((state) {
if (!mounted) return;
setState(() {
switch (state.status) {
case AuthStatus.initial:
_status = 'Not initialized';
case AuthStatus.loading:
_status = 'Authenticating...';
case AuthStatus.authenticated:
_status = 'Authenticated';
case AuthStatus.unauthenticated:
_status = state.error ?? 'Not authenticated';
}
});
});
return _auth!;
}
Future<void> _initialize() async {
try {
final auth = _getAuth();
await auth.initialize();
} catch (e) {
setState(() => _status = 'Initialization failed: $e');
}
}
Future<void> _loginWithSSO({bool privateMode = false}) async {
try {
final auth = _auth ?? _getAuth();
await auth.loginWithSSO(preferEphemeral: privateMode);
} catch (e) {
setState(() => _status = 'SSO login failed: $e');
}
}
Future<void> _loginWithDevice() async {
try {
final auth = _auth ?? _getAuth();
await auth.loginWithDevice(
onUserAction: (userCode, verificationUri) {
setState(() {
_status = 'Go to $verificationUri and enter code: $userCode';
});
},
onStatusUpdate: (status) {
setState(() => _status = 'Device auth: $status');
},
);
} catch (e) {
setState(() => _status = 'Device auth failed: $e');
}
}
Future<void> _testMe() async {
if (_auth == null || !_auth!.isAuthenticated) {
setState(() => _apiResponse = 'Please authenticate first');
return;
}
try {
setState(() => _apiResponse = 'Calling /me...');
final result = await _auth!.apiClient.me();
setState(() => _apiResponse = 'OK: $result');
} catch (e) {
setState(() => _apiResponse = 'FAIL: $e');
}
}
Future<void> _testListTasks() async {
if (_auth == null || !_auth!.isAuthenticated) {
setState(() => _apiResponse = 'Please authenticate first');
return;
}
try {
setState(() => _apiResponse = 'Fetching tasks...');
final result = await _auth!.apiClient.listTasks();
setState(() => _apiResponse = 'OK: $result');
} catch (e) {
setState(() => _apiResponse = 'FAIL: $e');
}
}
Future<void> _testListContent() async {
if (_auth == null || !_auth!.isAuthenticated) {
setState(() => _apiResponse = 'Please authenticate first');
return;
}
try {
setState(() => _apiResponse = 'Listing content...');
final result = await _auth!.apiClient.list('core.country');
setState(
() => _apiResponse = 'OK: ${result.count} countries');
} catch (e) {
setState(() => _apiResponse = 'FAIL: $e');
}
}
Future<void> _switchSession() async {
final current = _auth?.sessionId;
final next = current == 'session-a' ? 'session-b' : 'session-a';
try {
await _auth?.switchSession(next);
setState(() {});
} catch (e) {
setState(() => _status = 'Switch failed: $e');
}
}
Future<void> _logout() async {
try {
await _auth?.logout();
setState(() => _apiResponse = '');
} catch (e) {
setState(() => _status = 'Logout failed: $e');
}
}
@override
Widget build(BuildContext context) {
final isAuthenticated = _auth?.isAuthenticated ?? false;
final token = _auth?.currentToken;
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Bapp Auth Example'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Platform Info
Card(
color: Colors.blue[50],
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
key: const Key('platform_name'),
'Platform: ${PlatformConfig.platformName}',
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
key: const Key('redirect_uri'),
'Redirect URI: ${PlatformConfig.getRedirectUri()}',
style: const TextStyle(
fontSize: 12, fontFamily: 'monospace'),
),
],
),
),
),
const SizedBox(height: 16),
// Configuration
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Configuration',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
TextField(
key: const Key('client_id_field'),
controller: _clientIdController,
decoration: const InputDecoration(
labelText: 'Client ID (Keycloak)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
key: const Key('host_field'),
controller: _hostController,
decoration: const InputDecoration(
labelText: 'BAPP API Host',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
key: const Key('btn_initialize'),
onPressed: _initialize,
icon: const Icon(Icons.play_arrow),
label: const Text('Initialize'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 36),
),
),
],
),
),
),
const SizedBox(height: 16),
// Status
Text(
key: const Key('auth_status'),
_status,
style: TextStyle(
color: isAuthenticated ? Colors.green : Colors.orange,
fontWeight: FontWeight.w500,
fontSize: 16,
),
),
const SizedBox(height: 12),
// Authentication buttons
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Authentication',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
key: const Key('btn_sso'),
onPressed: !isAuthenticated
? () => _loginWithSSO()
: null,
icon: const Icon(Icons.login),
label: const Text('SSO Login'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
key: const Key('btn_sso_private'),
onPressed: !isAuthenticated
? () => _loginWithSSO(privateMode: true)
: null,
icon: const Icon(Icons.privacy_tip),
label: const Text('SSO (Private)'),
),
),
],
),
const SizedBox(height: 8),
ElevatedButton.icon(
key: const Key('btn_device'),
onPressed: !isAuthenticated ? _loginWithDevice : null,
icon: const Icon(Icons.devices),
label: const Text('Device Flow'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 36),
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
key: const Key('btn_switch_session'),
onPressed: _auth != null ? _switchSession : null,
icon: const Icon(Icons.swap_horiz),
label: Text(_auth?.sessionId != null
? 'Switch Session (current: ${_auth!.sessionId})'
: 'Switch Session'),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 36),
),
),
if (isAuthenticated) ...[
const SizedBox(height: 8),
ElevatedButton.icon(
key: const Key('btn_logout'),
onPressed: _logout,
icon: const Icon(Icons.logout),
label: const Text('Logout'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 36),
),
),
],
],
),
),
),
const SizedBox(height: 16),
// API Testing
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('API Testing',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
key: const Key('btn_me'),
onPressed: isAuthenticated ? _testMe : null,
child: const Text('Me'),
),
ElevatedButton(
key: const Key('btn_tasks'),
onPressed: isAuthenticated ? _testListTasks : null,
child: const Text('List Tasks'),
),
ElevatedButton(
key: const Key('btn_countries'),
onPressed: isAuthenticated ? _testListContent : null,
child: const Text('List Countries'),
),
],
),
const SizedBox(height: 12),
Container(
key: const Key('api_response'),
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(4),
),
child: Text(
_apiResponse.isEmpty ? '(no response)' : _apiResponse,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12),
),
),
],
),
),
),
// Token Info
if (token != null) ...[
const SizedBox(height: 16),
Card(
key: const Key('token_info'),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Token Info',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('Token Type: ${token.tokenType}'),
Text('Expires In: ${token.expiresIn}s'),
Text('Is Expired: ${token.isExpired}'),
Text('Is Expiring Soon: ${token.isExpiringSoon}'),
if (token.scope != null) Text('Scope: ${token.scope}'),
],
),
),
),
],
],
),
),
);
}
}