go_captcha_flutter 0.0.1
go_captcha_flutter: ^0.0.1 copied to clipboard
GoCaptcha Flutter library - A behavior captcha library for Flutter with Click, Slide, SlideRegion, and Rotate modes.
import 'package:flutter/material.dart';
import 'package:go_captcha_flutter/go_captcha_flutter.dart';
import 'api_service.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GoCaptcha Flutter Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GoCaptcha Flutter Example'),
),
body: IndexedStack(
index: _selectedIndex,
children: const [
ClickExample(),
SlideExample(),
SlideRegionExample(),
RotateExample(),
],
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.touch_app),
label: 'Click',
),
BottomNavigationBarItem(
icon: Icon(Icons.swipe),
label: 'Slide',
),
BottomNavigationBarItem(
icon: Icon(Icons.open_with),
label: 'SlideRegion',
),
BottomNavigationBarItem(
icon: Icon(Icons.rotate_right),
label: 'Rotate',
),
],
),
);
}
}
/// Click Captcha Example
class ClickExample extends StatefulWidget {
const ClickExample({Key? key}) : super(key: key);
@override
State<ClickExample> createState() => _ClickExampleState();
}
class _ClickExampleState extends State<ClickExample> {
String _image = '';
String _thumb = '';
String? _captchaBizKey;
bool _isLoading = false;
bool _isSubmitting = false;
List<ClickDot> _selectedDots = const [];
final GlobalKey<State<StatefulWidget>> _captchaKeyRef = GlobalKey();
@override
void initState() {
super.initState();
_loadCaptcha();
}
Future<void> _loadCaptcha() async {
setState(() {
_isLoading = true;
_image = '';
_thumb = '';
_captchaBizKey = null;
_selectedDots = const [];
});
try {
final result = await ApiService.generateClickCaptcha();
if (result['success'] == true) {
setState(() {
_image = result['imageBase64'] ?? '';
_thumb = result['thumbBase64'] ?? '';
_captchaBizKey = result['captchaKey'] as String?;
_isLoading = false;
});
(_captchaKeyRef.currentState as dynamic?)?.reset();
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: ${result['error'] ?? 'Unknown error'}'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleConfirm(List<ClickDot> dots, VoidCallback reset) async {
if (_captchaBizKey == null || dots.isEmpty || _isSubmitting) return;
setState(() {
_isSubmitting = true;
});
final points = dots
.map((dot) => {
'index': dot.index,
'x': dot.x.round(),
'y': dot.y.round(),
})
.toList();
final result = await ApiService.verifyClickCaptcha(_captchaBizKey!, points);
if (!mounted) return;
setState(() {
_isSubmitting = false;
});
final bool success = result['success'] == true && result['valid'] == true;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Verification passed'
: 'Verification failed: ${result['message'] ?? result['error'] ?? 'Please try again'}',
),
backgroundColor: success ? Colors.green : Colors.red,
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
reset();
await _loadCaptcha();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Click Captcha',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Request URL: ${ApiService.getBaseUrl()}/api/go-captcha-data/click-shape',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.black54,
),
),
const SizedBox(height: 20),
Column(
key: ValueKey('${_captchaBizKey ?? 'empty'}-${_image.isNotEmpty}-${_isLoading}'),
children: [
ClickCaptcha(
key: _captchaKeyRef,
config: const ClickConfig(
width: 320,
height: 210,
thumbWidth: 130,
thumbHeight: 36,
title: 'Click the targets in order',
buttonText: 'Verify now',
titleFontSize: 16,
buttonFontSize: 16,
iconSize: 24,
dotSize: 26,
),
image: _image,
thumb: _thumb,
onDotChanged: (dots) {
setState(() {
_selectedDots = dots;
});
},
onConfirm: _handleConfirm,
onRefresh: _loadCaptcha,
onClose: () {
(_captchaKeyRef.currentState as dynamic?)?.reset();
setState(() {
_selectedDots = const [];
});
},
),
const SizedBox(height: 14),
Container(
width: 326,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE7ECF5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current click coordinates',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_isLoading
? 'Loading captcha...'
: _selectedDots.isEmpty
? 'No points selected yet. Follow the prompt and click the targets in order.'
: _selectedDots
.map((dot) => '#${dot.index} (${dot.x.round()}, ${dot.y.round()})')
.join(' · '),
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.black87,
height: 1.5,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton.icon(
onPressed: _isLoading || _isSubmitting
? null
: () {
_loadCaptcha();
(_captchaKeyRef.currentState as dynamic?)?.reset();
},
icon: const Icon(Icons.refresh),
label: const Text('Reload'),
),
Container(
constraints: const BoxConstraints(maxWidth: 326),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFEFF4FF)
: const Color(0xFFF7F9FC),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: _isSubmitting
? const Color(0xFFBFD3FF)
: const Color(0xFFE3E9F3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isSubmitting)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
const Icon(
Icons.ads_click_outlined,
size: 18,
color: Color(0xFF4E87FF),
),
const SizedBox(width: 8),
Flexible(
child: Text(
_isSubmitting
? 'Verifying...'
: 'Confirm inside the captcha card',
softWrap: true,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF4B5563),
),
),
),
],
),
),
],
),
],
),
);
}
}
/// Slide Captcha Example
class SlideExample extends StatefulWidget {
const SlideExample({Key? key}) : super(key: key);
@override
State<SlideExample> createState() => _SlideExampleState();
}
class _SlideExampleState extends State<SlideExample> {
String _image = '';
String _thumb = '';
double _thumbX = 0.0;
double _thumbY = 0.0;
double _thumbWidth = 0.0;
double _thumbHeight = 0.0;
String? _captchaBizKey;
bool _isLoading = false;
bool _isSubmitting = false;
Position? _currentPosition;
final GlobalKey<State<StatefulWidget>> _captchaKeyRef = GlobalKey();
@override
void initState() {
super.initState();
_loadCaptcha();
}
Future<void> _loadCaptcha() async {
setState(() {
_isLoading = true;
_image = '';
_thumb = '';
_thumbX = 0.0;
_thumbY = 0.0;
_thumbWidth = 0.0;
_thumbHeight = 0.0;
_captchaBizKey = null;
_currentPosition = null;
});
try {
final result = await ApiService.generateSlideCaptcha();
if (result['success'] == true) {
setState(() {
_image = result['imageBase64'] ?? '';
_thumb = result['thumbBase64'] ?? '';
_thumbX = (result['tileX'] as num?)?.toDouble() ?? 0.0;
_thumbY = (result['tileY'] as num?)?.toDouble() ?? 0.0;
_thumbWidth = (result['tileWidth'] as num?)?.toDouble() ?? 0.0;
_thumbHeight = (result['tileHeight'] as num?)?.toDouble() ?? 0.0;
_captchaBizKey = result['captchaKey'] as String?;
_isLoading = false;
});
(_captchaKeyRef.currentState as dynamic?)?.reset();
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: ${result['error'] ?? 'Unknown error'}'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleConfirm(Position position, VoidCallback reset) async {
if (_captchaBizKey == null || _isSubmitting) return;
setState(() {
_isSubmitting = true;
_currentPosition = position;
});
final result = await ApiService.verifySlideCaptcha(
_captchaBizKey!,
position.x,
position.y,
);
if (!mounted) return;
setState(() {
_isSubmitting = false;
});
final bool success = result['success'] == true && result['valid'] == true;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Verification passed'
: 'Verification failed: ${result['message'] ?? result['error'] ?? 'Please try again'}',
),
backgroundColor: success ? Colors.green : Colors.red,
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
reset();
setState(() {
_currentPosition = null;
});
await _loadCaptcha();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Slide Captcha',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Request URL: ${ApiService.getBaseUrl()}/api/go-captcha-data/slide-basic',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.black54,
),
),
const SizedBox(height: 20),
Column(
key: ValueKey('${_captchaBizKey ?? 'empty'}-${_image.isNotEmpty}-${_isLoading}'),
children: [
SlideCaptcha(
key: _captchaKeyRef,
config: const SlideConfig(
width: 320,
height: 210,
title: 'Complete the puzzle by dragging the slider',
helperText: 'Hold the slider, drag right, and release to verify',
titleFontSize: 16,
helperFontSize: 12,
iconSize: 24,
),
image: _image,
thumb: _thumb,
thumbX: _thumbX,
thumbY: _thumbY,
thumbWidth: _thumbWidth,
thumbHeight: _thumbHeight,
onMove: (x, y) {
setState(() {
_currentPosition = Position(x: x, y: y);
});
},
onConfirm: _handleConfirm,
onRefresh: _loadCaptcha,
onClose: () {
(_captchaKeyRef.currentState as dynamic?)?.reset();
setState(() {
_currentPosition = null;
});
},
),
const SizedBox(height: 14),
Container(
width: 326,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE7ECF5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current slider position',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_isLoading
? 'Loading captcha...'
: _currentPosition == null
? 'Press and hold the blue slider below, then drag right to complete the puzzle'
: 'point=${_currentPosition!.x.round()},${_currentPosition!.y.round()}',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.black87,
height: 1.5,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton.icon(
onPressed: _isLoading || _isSubmitting
? null
: () {
_loadCaptcha();
(_captchaKeyRef.currentState as dynamic?)?.reset();
},
icon: const Icon(Icons.refresh),
label: const Text('Reload'),
),
Container(
constraints: const BoxConstraints(maxWidth: 326),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFEFF4FF)
: const Color(0xFFF7F9FC),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: _isSubmitting
? const Color(0xFFBFD3FF)
: const Color(0xFFE3E9F3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isSubmitting)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
const Icon(
Icons.swipe_rounded,
size: 18,
color: Color(0xFF4E87FF),
),
const SizedBox(width: 8),
Flexible(
child: Text(
_isSubmitting
? 'Verifying...'
: 'Verification runs automatically when the drag ends',
softWrap: true,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF4B5563),
),
),
),
],
),
),
],
),
],
),
);
}
}
/// SlideRegion Captcha Example
class SlideRegionExample extends StatefulWidget {
const SlideRegionExample({Key? key}) : super(key: key);
@override
State<SlideRegionExample> createState() => _SlideRegionExampleState();
}
class _SlideRegionExampleState extends State<SlideRegionExample> {
String _image = '';
String _thumb = '';
double _thumbX = 0.0;
double _thumbY = 0.0;
double _thumbWidth = 0.0;
double _thumbHeight = 0.0;
String? _captchaBizKey;
bool _isLoading = false;
bool _isSubmitting = false;
Position? _currentPosition;
final GlobalKey<State<StatefulWidget>> _captchaKeyRef = GlobalKey();
@override
void initState() {
super.initState();
_loadCaptcha();
}
Future<void> _loadCaptcha() async {
setState(() {
_isLoading = true;
_image = '';
_thumb = '';
_thumbX = 0.0;
_thumbY = 0.0;
_thumbWidth = 0.0;
_thumbHeight = 0.0;
_captchaBizKey = null;
_currentPosition = null;
});
try {
final result = await ApiService.generateSlideRegionCaptcha();
if (result['success'] == true) {
setState(() {
_image = result['imageBase64'] ?? '';
_thumb = result['thumbBase64'] ?? '';
_thumbX = (result['tileX'] as num?)?.toDouble() ?? 0.0;
_thumbY = (result['tileY'] as num?)?.toDouble() ?? 0.0;
_thumbWidth = (result['tileWidth'] as num?)?.toDouble() ?? 0.0;
_thumbHeight = (result['tileHeight'] as num?)?.toDouble() ?? 0.0;
_captchaBizKey = result['captchaKey'] as String?;
_isLoading = false;
});
(_captchaKeyRef.currentState as dynamic?)?.reset();
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: ${result['error'] ?? 'Unknown error'}'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleConfirm(Position position, VoidCallback reset) async {
if (_captchaBizKey == null || _isSubmitting) return;
setState(() {
_isSubmitting = true;
_currentPosition = position;
});
final result = await ApiService.verifySlideRegionCaptcha(
_captchaBizKey!,
position.x,
position.y,
);
if (!mounted) return;
setState(() {
_isSubmitting = false;
});
final bool success = result['success'] == true && result['valid'] == true;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Verification passed'
: 'Verification failed: ${result['message'] ?? result['error'] ?? 'Please try again'}',
),
backgroundColor: success ? Colors.green : Colors.red,
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
reset();
setState(() {
_currentPosition = null;
});
await _loadCaptcha();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'SlideRegion Captcha',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Request URL: ${ApiService.getBaseUrl()}/api/go-captcha-data/slide-region',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.black54,
),
),
const SizedBox(height: 20),
Column(
key: ValueKey('${_captchaBizKey ?? 'empty'}-${_image.isNotEmpty}-${_isLoading}'),
children: [
SlideRegionCaptcha(
key: _captchaKeyRef,
config: const SlideRegionConfig(
width: 320,
height: 210,
title: 'Move the puzzle piece into place',
idleText: 'Drag the piece freely and release it when it matches the gap',
draggingText: 'The current position will be submitted',
loadingText: 'Loading puzzle captcha, please wait',
titleFontSize: 16,
helperFontSize: 12,
iconSize: 22,
),
image: _image,
thumb: _thumb,
thumbX: _thumbX,
thumbY: _thumbY,
thumbWidth: _thumbWidth,
thumbHeight: _thumbHeight,
onMove: (x, y) {
setState(() {
_currentPosition = Position(x: x, y: y);
});
},
onConfirm: _handleConfirm,
onRefresh: _loadCaptcha,
onClose: () {
(_captchaKeyRef.currentState as dynamic?)?.reset();
setState(() {
_currentPosition = null;
});
},
),
const SizedBox(height: 14),
Container(
width: 326,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE7ECF5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current puzzle piece position',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_isLoading
? 'Loading captcha...'
: _currentPosition == null
? 'Drag the puzzle piece to the correct area and release it'
: 'point=${_currentPosition!.x.round()},${_currentPosition!.y.round()}',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.black87,
height: 1.5,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton.icon(
onPressed: _isLoading || _isSubmitting
? null
: () {
_loadCaptcha();
(_captchaKeyRef.currentState as dynamic?)?.reset();
},
icon: const Icon(Icons.refresh),
label: const Text('Reload'),
),
Container(
constraints: const BoxConstraints(maxWidth: 326),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFEFF4FF)
: const Color(0xFFF7F9FC),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: _isSubmitting
? const Color(0xFFBFD3FF)
: const Color(0xFFE3E9F3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isSubmitting)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
const Icon(
Icons.open_with_rounded,
size: 18,
color: Color(0xFF4E87FF),
),
const SizedBox(width: 8),
Flexible(
child: Text(
_isSubmitting
? 'Verifying...'
: 'Verification runs automatically when the drag ends',
softWrap: true,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF4B5563),
),
),
),
],
),
),
],
),
],
),
);
}
}
/// Rotate Captcha Example
class RotateExample extends StatefulWidget {
const RotateExample({Key? key}) : super(key: key);
@override
State<RotateExample> createState() => _RotateExampleState();
}
class _RotateExampleState extends State<RotateExample> {
String _image = '';
String _thumb = '';
double _angle = 0.0;
double _thumbSize = 0.0;
String? _captchaBizKey;
bool _isLoading = false;
bool _isSubmitting = false;
final GlobalKey<State<StatefulWidget>> _captchaKeyRef = GlobalKey();
@override
void initState() {
super.initState();
_loadCaptcha();
}
Future<void> _loadCaptcha() async {
setState(() {
_isLoading = true;
_image = '';
_thumb = '';
_angle = 0.0;
_thumbSize = 0.0;
_captchaBizKey = null;
});
try {
final result = await ApiService.generateRotateCaptcha();
if (result['success'] == true) {
setState(() {
_image = result['imageBase64'] ?? '';
_thumb = result['thumbBase64'] ?? '';
_angle = 0.0;
_thumbSize = (result['thumbSize'] as num?)?.toDouble() ?? 0.0;
_captchaBizKey = (result['captchaKey'] as String?)?.trim();
_isLoading = false;
});
(_captchaKeyRef.currentState as dynamic?)?.reset();
} else {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: ${result['error'] ?? 'Unknown error'}'),
backgroundColor: Colors.red,
),
);
}
}
} catch (e) {
setState(() {
_isLoading = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Load failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _handleConfirm(double angle, VoidCallback reset) async {
final String captchaKey = (_captchaBizKey ?? '').trim();
if (captchaKey.isEmpty || _isSubmitting) return;
setState(() {
_isSubmitting = true;
_angle = angle;
});
final result = await ApiService.verifyRotateCaptcha(captchaKey, angle);
if (!mounted) return;
setState(() {
_isSubmitting = false;
});
final bool success = result['success'] == true && result['valid'] == true;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success
? 'Verification passed'
: 'Verification failed: ${result['message'] ?? result['error'] ?? 'Please try again'}',
),
backgroundColor: success ? Colors.green : Colors.red,
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
reset();
await _loadCaptcha();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Rotate Captcha',
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
'Request URL: ${ApiService.getBaseUrl()}/api/go-captcha-data/rotate-basic',
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.black54,
),
),
const SizedBox(height: 20),
Column(
key: ValueKey('${_captchaBizKey ?? 'empty'}-${_image.isNotEmpty}-${_isLoading}'),
children: [
RotateCaptcha(
key: _captchaKeyRef,
config: RotateConfig(
width: 320,
height: 250,
size: 220,
thumbSize: _thumbSize > 0 ? _thumbSize : 0.0,
title: 'Rotate the image to the correct angle',
helperText: 'Drag the slider below to adjust the rotation angle',
titleFontSize: 16,
helperFontSize: 12,
iconSize: 24,
),
image: _image,
thumb: _thumb,
angle: 0.0,
onRotate: (angle) {
setState(() {
_angle = angle;
});
},
onConfirm: _handleConfirm,
onRefresh: _loadCaptcha,
onClose: () {
(_captchaKeyRef.currentState as dynamic?)?.reset();
setState(() {
_angle = 0.0;
});
},
),
const SizedBox(height: 14),
Container(
width: 326,
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE7ECF5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current rotation angle',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
Text(
_isLoading
? 'Loading captcha...'
: 'angle=${_angle.round()}°\nkey=${_captchaBizKey ?? ''}',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.black87,
height: 1.5,
),
),
],
),
),
],
),
const SizedBox(height: 16),
Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 12,
children: [
OutlinedButton.icon(
onPressed: _isLoading || _isSubmitting
? null
: () {
_loadCaptcha();
(_captchaKeyRef.currentState as dynamic?)?.reset();
},
icon: const Icon(Icons.refresh),
label: const Text('Reload'),
),
Container(
constraints: const BoxConstraints(maxWidth: 326),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _isSubmitting
? const Color(0xFFEFF4FF)
: const Color(0xFFF7F9FC),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: _isSubmitting
? const Color(0xFFBFD3FF)
: const Color(0xFFE3E9F3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isSubmitting)
const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
else
const Icon(
Icons.rotate_right_rounded,
size: 18,
color: Color(0xFF4E87FF),
),
const SizedBox(width: 8),
Flexible(
child: Text(
_isSubmitting
? 'Verifying...'
: 'Verification runs automatically when the drag ends',
softWrap: true,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF4B5563),
),
),
),
],
),
),
],
),
],
),
);
}
}