captchala 1.3.2 copy "captchala: ^1.3.2" to clipboard
captchala: ^1.3.2 copied to clipboard

Captchala SDK for Flutter — risk-driven captcha verification with native UI on iOS / Android / macOS / Linux / Windows.

example/lib/main.dart

// Captchala Flutter demo.
//
// A simple panel that lets you pick action / lang / theme and run the
// full verify flow. In a real integration:
//   1. Your backend issues a one-time server_token
//      (see https://captcha.la/docs).
//   2. Pass it to CaptchalaConfig.serverToken.
//   3. Call client.verify() — native captcha UI opens.
//   4. On success, send result.passToken back to your backend for
//      validation.

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:captchala/captchala.dart';
import 'package:http/http.dart' as http;

// -------- Demo configuration --------------------------------------------
// Replace with your own appKey from https://dash.captcha.la.
const String kDemoAppKey = 'demo_app';
// Demo backend that issues server_token and validates pass_token.
// In production these endpoints live on YOUR backend.
const String kDemoApiServer = 'https://demo-v1.captcha.la';
// Integrator business UID — comes from your authenticated session.
const String kDemoUid = 'demo-user-12345';

// -------- Picker option lists (match Android / iOS demos) --------------


const List<String> _actions = ['login', 'register', 'pay'];

const List<({String label, String value})> _languages = [
  (label: 'Auto (system)',    value: ''),
  (label: '简体中文',         value: 'zh-CN'),
  (label: '繁體中文',         value: 'zh-TW'),
  (label: 'English',          value: 'en'),
  (label: '日本語',           value: 'ja'),
  (label: '한국어',           value: 'ko'),
  (label: 'Bahasa Melayu',    value: 'ms'),
  (label: 'Tiếng Việt',       value: 'vi'),
  (label: 'Bahasa Indonesia', value: 'id'),
];

const List<String> _themes = ['light', 'dark', 'system'];

// -------- Demo backend helpers ------------------------------------------

/// Fetch a one-time server_token from the integrator backend (demo backend used here).
Future<String?> fetchServerToken({required String appKey, required String action}) async {
  try {
    final uri = Uri.parse(
      '$kDemoApiServer/demo/issue-captcha-token'
      '?app_key=$appKey&action=$action&uid=$kDemoUid',
    );
    final res = await http.get(uri).timeout(const Duration(seconds: 8));
    if (res.statusCode != 200) return null;
    final body = jsonDecode(res.body) as Map<String, dynamic>?;
    final data = body?['data'] as Map<String, dynamic>?;
    final token = data?['server_token'] as String?;
    return (token != null && token.isNotEmpty) ? token : null;
  } catch (_) {
    return null;
  }
}

/// Simulates integrator backend validating the pass_token.
Future<String> validatePassToken({required String appKey, required String passToken}) async {
  try {
    final uri = Uri.parse('$kDemoApiServer/demo/validate-pass-token');
    final res = await http.post(uri, body: {
      'app_key': appKey,
      'pass_token': passToken,
      'expected_uid': kDemoUid,
    }).timeout(const Duration(seconds: 8));
    final body = jsonDecode(res.body) as Map<String, dynamic>?;
    final data = body?['data'] as Map<String, dynamic>?;
    if (data == null) return 'validate failed: no data';
    final valid = data['valid'] == true;
    if (!valid) return 'validate: invalid — ${data['error'] ?? 'unknown'}';
    final uid = (data['uid'] as String?) ?? '(null)';
    final match = data['uid_match'] == true;
    return 'validate: valid=true, uid=$uid, match=${match ? "✓" : "✗"}';
  } catch (e) {
    return 'validate failed: $e';
  }
}

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'captchala example',
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      home: const ExampleHome(),
    );
  }
}

class ExampleHome extends StatefulWidget {
  const ExampleHome({super.key});

  @override
  State<ExampleHome> createState() => _ExampleHomeState();
}

class _ExampleHomeState extends State<ExampleHome> {
  // -------- Settings state (mirrors Android / iOS demos) ----------------

  String _action = 'login';
  String _lang = ''; // Auto (system) by default
  String _theme = 'light';
  bool _enableVoice = true;
  bool _enableOfflineMode = false;
  bool _maskClosable = false;

  // -------- Result state ------------------------------------------------

  String _status = 'Configure settings, then tap Verify.';
  bool _isVerifying = false;

  // -------- MethodChannel path ------------------------------------------

  Future<void> _runVerify() async {
    setState(() {
      _isVerifying = true;
      _status = 'Fetching server_token...';
    });

    try {
      // 1) Pull a one-shot server_token from the integrator backend.
      final initialToken = await fetchServerToken(appKey: kDemoAppKey, action: _action);
      if (!mounted) return;
      setState(() => _status = initialToken != null
          ? 'Got server_token, starting verify()...'
          : '⚠︎ server_token missing, continuing without bind (prod should abort)');

      // 2) Build config from current panel state.
      final config = CaptchalaConfig(
        appKey: kDemoAppKey,
        action: _action,
        lang: _lang,
        theme: _theme,
        enableVoice: _enableVoice,
        enableOfflineMode: _enableOfflineMode,
        maskClosable: _maskClosable,
        serverToken: initialToken,
      );

      final client = await CaptchalaClient.instance.initialize(config);
      debugPrint('[demo] verify() invoked');
      client.setCallbacks(
        onSuccess: (r) async {
          debugPrint('[demo] onSuccess fired'
              ' isOffline=${r.isOffline}'
              ' isClientOnly=${r.isClientOnly}'
              ' passToken=${r.passToken}'
              ' challengeId=${r.challengeId}');
          if (!mounted) return;
          final flags = <String>[
            if (r.isOffline) 'OFFLINE',
            if (r.isClientOnly) 'CLIENT-ONLY',
          ];
          final tag = flags.isEmpty ? '' : ' [${flags.join(', ')}]';
          setState(() => _status =
              'onSuccess$tag: token=${r.passToken}\n(validating with backend...)');
          final info = await validatePassToken(appKey: kDemoAppKey, passToken: r.passToken);
          if (!mounted) return;
          setState(() {
            _status = 'onSuccess$tag: token=${r.passToken}\n$info';
            _isVerifying = false;
          });
        },
        onFail: (e) {
          debugPrint('[demo] onFail fired code=${e.code} message=${e.message}');
          if (!mounted) return;
          setState(() {
            _status = 'onFail: ${e.code} ${e.message}';
            _isVerifying = false;
          });
        },
        onError: (e) {
          debugPrint('[demo] onError fired code=${e.code} message=${e.message}');
          if (!mounted) return;
          setState(() {
            _status = 'onError: ${e.code} ${e.message}';
            _isVerifying = false;
          });
        },
        onClose: () {
          debugPrint('[demo] onClose fired (was _isVerifying=$_isVerifying)');
          if (!mounted) return;
          setState(() {
            if (_isVerifying) {
              _status = 'onClose (closed before completion)';
            }
            _isVerifying = false;
          });
        },
        onServerTokenExpired: () {
          debugPrint('[demo] onServerTokenExpired fired — fetching new token');
          return fetchServerToken(appKey: kDemoAppKey, action: _action);
        },
      );
      debugPrint('[demo] calling client.verify()...');
      await client.verify();
      debugPrint('[demo] client.verify() returned (waiting for callbacks)');
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _status = 'exception: $e';
        _isVerifying = false;
      });
    }
  }

  // -------- UI ----------------------------------------------------------

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Captchala Demo')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // --- Captcha settings -------------------------------------------
          const Text('Captcha Settings',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
          const SizedBox(height: 12),

          _buildLabeledPicker(
            label: 'Action',
            value: _action,
            options: _actions.map((e) => (label: e, value: e)).toList(),
            onChanged: (v) => setState(() => _action = v),
          ),
          _buildLabeledPicker(
            label: 'Language',
            value: _lang,
            options: _languages.map((e) => (label: e.label, value: e.value)).toList(),
            onChanged: (v) => setState(() => _lang = v),
          ),
          _buildLabeledPicker(
            label: 'Theme',
            value: _theme,
            options: _themes.map((e) => (label: e, value: e)).toList(),
            onChanged: (v) => setState(() => _theme = v),
          ),

          const SizedBox(height: 8),
          const Divider(),
          const SizedBox(height: 4),

          // --- Toggles ---------------------------------------------------
          const Text('Toggles',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
          SwitchListTile(
            title: const Text('Enable voice fallback'),
            value: _enableVoice,
            onChanged: (v) => setState(() => _enableVoice = v),
            contentPadding: EdgeInsets.zero,
          ),
          SwitchListTile(
            title: const Text('Allow offline mode'),
            value: _enableOfflineMode,
            onChanged: (v) => setState(() => _enableOfflineMode = v),
            contentPadding: EdgeInsets.zero,
          ),
          SwitchListTile(
            title: const Text('Mask click closes'),
            value: _maskClosable,
            onChanged: (v) => setState(() => _maskClosable = v),
            contentPadding: EdgeInsets.zero,
          ),
          const SizedBox(height: 16),
          const Divider(),
          const SizedBox(height: 12),

          // --- Verify button --------------------------------------------
          ElevatedButton.icon(
            onPressed: _isVerifying ? null : _runVerify,
            icon: _isVerifying
                ? const SizedBox(
                    width: 18,
                    height: 18,
                    child: CircularProgressIndicator(strokeWidth: 2))
                : const Icon(Icons.verified_user),
            label: Text(_isVerifying ? 'Verifying…' : 'Verify with Captcha'),
            style: ElevatedButton.styleFrom(
              minimumSize: const Size.fromHeight(48),
            ),
          ),
          const SizedBox(height: 8),
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey.shade50,
              border: Border.all(color: Colors.grey.shade300),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(_status, style: const TextStyle(fontFamily: 'monospace')),
          ),

          const SizedBox(height: 24),

          // --- Build info -----------------------------------------------
          const Text('Build Info',
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
          const SizedBox(height: 8),
          _buildInfoRow('appKey', kDemoAppKey),
          _buildInfoRow('uid',    kDemoUid),
          _buildInfoRow('apiServer', kDemoApiServer),
        ],
      ),
    );
  }

  Widget _buildLabeledPicker({
    required String label,
    required String value,
    required List<({String label, String value})> options,
    required void Function(String) onChanged,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Row(
        children: [
          SizedBox(width: 100, child: Text(label)),
          const SizedBox(width: 8),
          Expanded(
            child: DropdownButtonFormField<String>(
              value: value,
              isDense: true,
              decoration: const InputDecoration(
                isDense: true,
                contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                border: OutlineInputBorder(),
              ),
              items: options
                  .map((o) =>
                      DropdownMenuItem(value: o.value, child: Text(o.label)))
                  .toList(),
              onChanged: (v) {
                if (v != null) onChanged(v);
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String key, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        children: [
          SizedBox(
              width: 80,
              child:
                  Text(key, style: const TextStyle(color: Colors.grey))),
          Expanded(
            child: SelectableText(
              value,
              style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
            ),
          ),
        ],
      ),
    );
  }
}
3
likes
150
points
142
downloads

Documentation

Documentation
API reference

Publisher

verified publishercaptcha.la

Weekly Downloads

Captchala SDK for Flutter — risk-driven captcha verification with native UI on iOS / Android / macOS / Linux / Windows.

Homepage

License

MIT (license)

Dependencies

flutter

More

Packages that depend on captchala

Packages that implement captchala