flutter_secure_auth 0.1.8
flutter_secure_auth: ^0.1.8 copied to clipboard
A secure, lightweight authentication package for Flutter (REST + OAuth2 PKCE + token refresh) with secure local storage.
import 'package:flutter/material.dart';
import 'package:flutter_secure_auth/flutter_secure_auth.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() => runApp(const PetStoreApp());
/// A demo product class representing items in the pet store.
class Product {
final String id;
final String name;
final String category;
final double price;
final String icon;
final String description;
const Product({
required this.id,
required this.name,
required this.category,
required this.price,
required this.icon,
required this.description,
});
}
/// The main application entry point showcasing the Pet Supplies Store MVP.
class PetStoreApp extends StatelessWidget {
const PetStoreApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Paws & Claws Supplies',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2A2D43),
primary: const Color(0xFF2A2D43),
secondary: const Color(0xFFF3A712),
background: const Color(0xFFF9F9FB),
),
cardTheme: const CardTheme(
elevation: 2,
color: Colors.white,
),
),
home: const PetStoreCatalogScreen(),
);
}
}
/// The main catalog screen of the pet store featuring product grids,
/// category filters, and a slide-out cart sheet.
class PetStoreCatalogScreen extends StatefulWidget {
const PetStoreCatalogScreen({super.key});
@override
State<PetStoreCatalogScreen> createState() => _PetStoreCatalogScreenState();
}
class _AuthMockClient extends http.BaseClient {
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// Return a mocked successful authentication response for demonstration
final payload = {
'access_token': 'mocked_jwt_token_for_pet_store_checkout',
'refresh_token': 'mocked_refresh_token',
'expires_in': 3600,
};
return http.StreamedResponse(
Stream.fromIterable([utf8.encode(jsonEncode(payload))]),
200,
headers: {'content-type': 'application/json'},
);
}
}
class _PetStoreCatalogScreenState extends State<PetStoreCatalogScreen> {
// Initialize our newly updated secure auth service
late final AuthService authService;
final List<Product> _products = const [
Product(
id: 'p1',
name: 'Premium Puppy Kibble',
category: 'Dogs',
price: 49.99,
icon: '🐶',
description: 'Organic grain-free kibble rich in proteins for healthy puppy growth.',
),
Product(
id: 'p2',
name: 'Cat Feather wand Toy',
category: 'Cats',
price: 12.49,
icon: '🐱',
description: 'Interactive teaser wand to keep your cat active and entertained.',
),
Product(
id: 'p3',
name: 'Orchard Grass Hay',
category: 'Rabbits',
price: 18.99,
icon: '🐰',
description: 'Sweet, soft hay ideal for rabbits, guinea pigs, and small pets.',
),
Product(
id: 'p4',
name: 'Ergonomic Dog Harness',
category: 'Dogs',
price: 34.99,
icon: '🐕',
description: 'No-pull breathable chest harness with reflective security strips.',
),
Product(
id: 'p5',
name: 'Self-Cleaning Litter Mat',
category: 'Cats',
price: 22.99,
icon: '🐈',
description: 'Double-layer honeycomb design traps litter scatter effectively.',
),
Product(
id: 'p6',
name: 'Premium Bird Seed Mix',
category: 'Birds',
price: 14.99,
icon: '🦜',
description: 'Natural blend of seeds, nuts, and dried fruits for colorful birds.',
),
];
final Map<Product, int> _cart = {};
String _selectedCategory = 'All';
bool _isCheckingOut = false;
AuthTokens? _userSession;
@override
void initState() {
super.initState();
authService = AuthService(
tokenEndpoint: Uri.parse('https://api.example.com/oauth/token'),
httpClientFactory: () => _AuthMockClient(),
);
_checkExistingSession();
}
Future<void> _checkExistingSession() async {
final tokens = await authService.currentTokens();
if (mounted) {
setState(() => _userSession = tokens);
}
}
void _addToCart(Product product) {
setState(() {
_cart[product] = (_cart[product] ?? 0) + 1;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} added to cart!'),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
backgroundColor: const Color(0xFF2A2D43),
),
);
}
void _removeFromCart(Product product) {
setState(() {
if (_cart.containsKey(product)) {
if (_cart[product] == 1) {
_cart.remove(product);
} else {
_cart[product] = _cart[product]! - 1;
}
}
});
}
double get _cartTotal =>
_cart.entries.fold(0, (sum, entry) => sum + (entry.key.price * entry.value));
int get _cartItemCount =>
_cart.values.fold(0, (sum, count) => sum + count);
Future<void> _handleCheckout() async {
// If not logged in, trigger auth flow using flutter_secure_auth
if (_userSession == null) {
final success = await _showLoginDialog();
if (!success) return;
}
setState(() => _isCheckingOut = true);
// Simulate ordering network request delay
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isCheckingOut = false;
_cart.clear();
});
showDialog(
context: context,
builder: (context) => AlertDialog(
icon: const Icon(Icons.check_circle, size: 60, color: Colors.green),
title: const Text('Order Confirmed!'),
content: const Text(
'Your checkout was completed securely. We have sent a confirmation details invoice to your email.',
textAlign: TextAlign.center,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Great'),
),
],
),
);
}
}
Future<bool> _showLoginDialog() async {
final usernameController = TextEditingController(text: 'customer@petstore.com');
final passwordController = TextEditingController(text: 'securepassword123');
bool isLoading = false;
String? errorMessage;
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Row(
children: [
Icon(Icons.lock_outline, color: Theme.of(context).primaryColor),
const SizedBox(width: 10),
const Text('Secure Checkout Login'),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'To complete your order, log in or sign up securely. (Try submitting the mocked form below!)',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 16),
TextField(
controller: usernameController,
decoration: const InputDecoration(
labelText: 'Email Address',
prefixIcon: Icon(Icons.email_outlined),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outline),
border: OutlineInputBorder(),
),
),
if (errorMessage != null) ...[
const SizedBox(height: 12),
Text(
errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
],
if (isLoading) ...[
const SizedBox(height: 16),
const CircularProgressIndicator(),
],
],
),
actions: [
TextButton(
onPressed: isLoading ? null : () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: isLoading
? null
: () async {
setDialogState(() {
isLoading = true;
errorMessage = null;
});
try {
final tokens = await authService.signInWithPassword(
endpoint: Uri.parse('https://api.example.com/login'),
username: usernameController.text,
password: passwordController.text,
);
if (context.mounted) {
setState(() => _userSession = tokens);
Navigator.pop(context, true);
}
} catch (e) {
setDialogState(() {
isLoading = false;
errorMessage = 'Authentication failed: $e';
});
}
},
child: const Text('Log In & Checkout'),
),
],
);
},
);
},
);
return result ?? false;
}
Future<void> _handleLogout() async {
await authService.signOut();
setState(() {
_userSession = null;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Logged out successfully')),
);
}
}
@override
Widget build(BuildContext context) {
final filteredProducts = _selectedCategory == 'All'
? _products
: _products.where((p) => p.category == _selectedCategory).toList();
return Scaffold(
appBar: AppBar(
title: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Paws & Claws Supplies',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
Text(
'Pet Store App MVP Demo',
style: TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
actions: [
if (_userSession != null)
IconButton(
icon: const Icon(Icons.logout, color: Colors.redAccent),
tooltip: 'Log out secure session',
onPressed: _handleLogout,
)
else
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Icon(Icons.lock_open, color: Colors.grey),
)
],
),
body: Column(
children: [
// Category Selector
Container(
height: 60,
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListView(
scrollDirection: Axis.horizontal,
children: ['All', 'Dogs', 'Cats', 'Birds', 'Rabbits'].map((category) {
final isSelected = _selectedCategory == category;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ChoiceChip(
label: Text(category),
selected: isSelected,
selectedColor: const Color(0xFF2A2D43),
labelStyle: TextStyle(
color: isSelected ? Colors.white : Colors.black87,
fontWeight: FontWeight.bold,
),
onSelected: (val) {
setState(() => _selectedCategory = category);
},
),
);
}).toList(),
),
),
// Product Grid Listing
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.72,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: filteredProducts.length,
itemBuilder: (context, index) {
final product = filteredProducts[index];
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Cover / Icon Box
Expanded(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.primary.withOpacity(0.04),
alignment: Alignment.center,
child: Text(
product.icon,
style: const TextStyle(fontSize: 60),
),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
product.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 11,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
Text(
'\$${product.price.toStringAsFixed(2)}',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xFFF3A712),
fontSize: 15,
),
),
IconButton(
icon: const Icon(Icons.add_shopping_cart),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
color: const Color(0xFF2A2D43),
onPressed: () => _addToCart(product),
),
],
),
],
),
),
],
),
);
},
),
),
],
),
bottomNavigationBar: _cart.isEmpty
? null
: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, -4),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Total ($_cartItemCount Items)',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(
'\$${_cartTotal.toStringAsFixed(2)}',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF2A2D43),
),
),
],
),
ElevatedButton(
onPressed: () {
_showCartSheet();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFF3A712),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text(
'View Cart & Checkout',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
);
}
void _showCartSheet() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return StatefulBuilder(
builder: (context, setSheetState) {
return DraggableScrollableSheet(
initialChildSize: 0.6,
maxChildSize: 0.9,
minChildSize: 0.4,
expand: false,
builder: (context, scrollController) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
const Text(
'Your Cart',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
if (_userSession != null)
const Chip(
avatar: Icon(Icons.verified, size: 16, color: Colors.green),
label: Text('Secure Session Active', style: TextStyle(fontSize: 10)),
),
],
),
const SizedBox(height: 16),
Expanded(
child: _cart.isEmpty
? const Center(child: Text('Your cart is empty.'))
: ListView.builder(
controller: scrollController,
itemCount: _cart.length,
itemBuilder: (context, index) {
final product = _cart.keys.elementAt(index);
final quantity = _cart[product]!;
return ListTile(
leading: Text(product.icon, style: const TextStyle(fontSize: 28)),
title: Text(product.name),
subtitle: Text('\$${product.price} x $quantity'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
_removeFromCart(product);
setSheetState(() {});
setState(() {});
},
),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
_addToCart(product);
setSheetState(() {});
setState(() {});
},
),
],
),
);
},
),
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.between,
children: [
const Text('Total amount:', style: TextStyle(fontWeight: FontWeight.bold)),
Text(
'\$${_cartTotal.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _cart.isEmpty || _isCheckingOut
? null
: () async {
Navigator.pop(context); // Close bottom sheet
await _handleCheckout();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2A2D43),
foregroundColor: Colors.white,
),
child: _isCheckingOut
? const CircularProgressIndicator(color: Colors.white)
: const Text('Proceed to Secure Checkout', style: TextStyle(fontWeight: FontWeight.bold)),
),
),
],
),
);
},
);
},
);
},
);
}
}