go_captcha_flutter 0.0.1 copy "go_captcha_flutter: ^0.0.1" to clipboard
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.

example/lib/main.dart

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),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
3
likes
145
points
30
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

GoCaptcha Flutter library - A behavior captcha library for Flutter with Click, Slide, SlideRegion, and Rotate modes.

Repository (GitHub)
View/report issues

Topics

#captcha #flutter #security #widget

License

MIT (license)

Dependencies

flutter

More

Packages that depend on go_captcha_flutter