seon_orchestration_flutter 1.0.3 copy "seon_orchestration_flutter: ^1.0.3" to clipboard
seon_orchestration_flutter: ^1.0.3 copied to clipboard

Flutter plugin wrapping the SEON Orchestration SDK for identity verification.

example/lib/main.dart

import 'package:app_settings/app_settings.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:seon_orchestration_flutter/seon_orchestration_flutter.dart';

// ─── Regional base URLs (aligned with seon-orchestration-sdk-react-native-public) ─
const Map<SeonRegion, String> kBaseUrls = {
  SeonRegion.eu: 'https://api.seon.io/orchestration-api/',
  SeonRegion.us: 'https://api.us-east-1-main.seon.io/orchestration-api/',
  SeonRegion.apac: 'https://api.ap-southeast-1-main.seon.io/orchestration-api/',
  SeonRegion.dev: 'https://api.seondev.space/orchestration-api/v1',
};

enum SeonRegion { eu, us, apac, dev }

enum _AppPhase { idle, initializing, verifying, done }

class SeonStatusMeta {
  final String label;
  final Color color;
  final String icon;

  const SeonStatusMeta({required this.label, required this.color, required this.icon});
}

const Map<SeonVerificationStatus, SeonStatusMeta> kStatusMeta = {
  SeonVerificationStatus.completedSuccess: SeonStatusMeta(
    label: 'Verification Passed',
    color: Color(0xFF22C55E),
    icon: '✓',
  ),
  SeonVerificationStatus.completedPending: SeonStatusMeta(
    label: 'Pending Manual Review',
    color: Color(0xFFF59E0B),
    icon: '⏳',
  ),
  SeonVerificationStatus.completedFailed: SeonStatusMeta(
    label: 'Verification Failed',
    color: Color(0xFFEF4444),
    icon: '✗',
  ),
  SeonVerificationStatus.completed: SeonStatusMeta(
    label: 'Verification Completed',
    color: Color(0xFF3B82F6),
    icon: '✓',
  ),
  SeonVerificationStatus.interruptedByUser: SeonStatusMeta(
    label: 'Cancelled by User',
    color: Color(0xFF6B7280),
    icon: '↩',
  ),
  SeonVerificationStatus.missingLocationPermission: SeonStatusMeta(
    label: 'Location Permission Required',
    color: Color(0xFFF97316),
    icon: '📍',
  ),
  SeonVerificationStatus.error: SeonStatusMeta(
    label: 'Error Occurred',
    color: Color(0xFFDC2626),
    icon: '!',
  ),
};

// ─── Design tokens (aligned with RN sample) ─────────────────────────────────────
abstract final class _C {
  static const primary = Color(0xFF1E40AF);
  static const background = Color(0xFFF1F5F9);
  static const card = Color(0xFFFFFFFF);
  static const text = Color(0xFF0F172A);
  static const subtext = Color(0xFF64748B);
  static const placeholder = Color(0xFF94A3B8);
  static const border = Color(0xFFE2E8F0);
  static const neutral = Color(0xFFCBD5E1);
}

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const SeonExampleApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SEON-Orch-Flutter',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: _C.primary),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  SeonRegion _region = SeonRegion.eu;
  final TextEditingController _tokenController = TextEditingController();
  final TextEditingController _languageController = TextEditingController(text: 'en');

  _AppPhase _phase = _AppPhase.idle;
  String _statusMessage = 'Enter your token to begin';
  SeonVerificationResult? _result;
  DateTime? _resultAt;

  bool get _isBusy => _phase == _AppPhase.initializing || _phase == _AppPhase.verifying;

  @override
  void dispose() {
    _tokenController.dispose();
    _languageController.dispose();
    super.dispose();
  }

  Future<void> _showAlert(String title, String body) async {
    if (!mounted) return;
    await showDialog<void>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('OK')),
        ],
      ),
    );
  }

  String _mapInitError(SeonException e) {
    switch (e.code) {
      case SeonErrorCode.eNotInitialized:
        return 'SDK not initialized. Please try again.';
      case SeonErrorCode.eInitializationFailed:
        return 'Initialization failed. Check your token and base URL.';
      case SeonErrorCode.eVerificationInProgress:
        return 'A verification is already in progress.';
      default:
        return e.message;
    }
  }

  Future<void> _runVerification() async {
    final token = _tokenController.text.trim();
    if (token.isEmpty) {
      await _showAlert('Token Required', 'Please enter your session token to proceed.');
      return;
    }

    setState(() {
      _phase = _AppPhase.initializing;
      _statusMessage = 'Initializing SDK…';
      _result = null;
      _resultAt = null;
    });

    final baseUrl = kBaseUrls[_region]!;
    final lang = _languageController.text.trim();
    final language = lang.isEmpty ? 'en' : lang;

    try {
      await SeonOrchestration.initialize(
        SeonConfig(
          baseUrl: baseUrl,
          token: token,
          language: language,
        ),
      );

      if (!mounted) return;
      setState(() {
        _phase = _AppPhase.verifying;
        _statusMessage = 'Launching verification flow…';
      });

      final result = await SeonOrchestration.startVerification();

      if (!mounted) return;
      setState(() {
        _phase = _AppPhase.done;
        _result = result;
        _resultAt = DateTime.now();
        _statusMessage = kStatusMeta[result.status]?.label ?? result.status.name;
      });

      final s = result.status;
      if (s == SeonVerificationStatus.completedSuccess) {
        await _showAlert('Identity Verified', 'Your identity has been successfully verified.');
      } else if (s == SeonVerificationStatus.completedPending) {
        await _showAlert(
          'Under Review',
          'Your submission is under review. You will be notified once a decision is made.',
        );
      } else if (s == SeonVerificationStatus.completedFailed) {
        await _showAlert(
          'Verification Failed',
          'We could not verify your identity. Please try again or contact support.',
        );
      } else if (s == SeonVerificationStatus.interruptedByUser) {
        setState(() {
          _phase = _AppPhase.idle;
          _statusMessage = 'Verification cancelled. Tap below to try again.';
          _result = null;
          _resultAt = null;
        });
      } else if (s == SeonVerificationStatus.missingLocationPermission) {
        if (mounted) {
          await showDialog<void>(
            context: context,
            builder: (ctx) => AlertDialog(
              title: const Text('Location Required'),
              content: const Text(
                'Location access is required for fraud prevention. Please enable it in your device settings.',
              ),
              actions: [
                TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel')),
                TextButton(
                  onPressed: () {
                    Navigator.of(ctx).pop();
                    AppSettings.openAppSettings();
                  },
                  child: const Text('Open Settings'),
                ),
              ],
            ),
          );
        }
      } else if (s == SeonVerificationStatus.error) {
        await _showAlert('Error', result.errorMessage ?? 'An unexpected error occurred.');
      }
    } on SeonException catch (e) {
      if (!mounted) return;
      setState(() {
        _phase = _AppPhase.idle;
        _statusMessage = 'Failed. Check your configuration and try again.';
      });
      await _showAlert('Error', _mapInitError(e));
    } catch (e) {
      if (!mounted) return;
      setState(() {
        _phase = _AppPhase.idle;
        _statusMessage = 'Failed. Check your configuration and try again.';
      });
      await _showAlert('Error', e.toString());
    }
  }

  void _reset() {
    setState(() {
      _phase = _AppPhase.idle;
      _result = null;
      _resultAt = null;
      _statusMessage = 'Enter your token to begin';
    });
  }

  String _platformLine() {
    if (kIsWeb) return 'Web';
    return switch (defaultTargetPlatform) {
      TargetPlatform.iOS => 'iOS',
      TargetPlatform.android => 'Android',
      TargetPlatform.fuchsia => 'Fuchsia',
      TargetPlatform.linux => 'Linux',
      TargetPlatform.macOS => 'macOS',
      TargetPlatform.windows => 'Windows',
    };
  }

  @override
  Widget build(BuildContext context) {
    final meta = _result != null ? kStatusMeta[_result!.status] : null;

    return Scaffold(
      backgroundColor: _C.primary,
      body: SafeArea(
        child: Column(
          children: [
            const _Header(),
            Expanded(
              child: ColoredBox(
                color: _C.background,
                child: ListView(
                  padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
                  keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
                  children: [
                    _ConfigurationCard(
                      region: _region,
                      onRegion: _isBusy
                          ? null
                          : (r) => setState(() => _region = r),
                      baseUrlHint: kBaseUrls[_region]!,
                      tokenController: _tokenController,
                      languageController: _languageController,
                      inputsEnabled: !_isBusy,
                    ),
                    const SizedBox(height: 14),
                    _StatusCard(
                      isBusy: _isBusy,
                      statusMessage: _statusMessage,
                      metaColor: meta?.color,
                    ),
                    if (_result != null && meta != null && _resultAt != null) ...[
                      const SizedBox(height: 14),
                      _ResultCard(
                        meta: meta,
                        result: _result!,
                        at: _resultAt!,
                        platformLine: _platformLine(),
                      ),
                    ],
                    const SizedBox(height: 14),
                    const _InfoBanner(),
                  ],
                ),
              ),
            ),
            _Footer(
              isBusy: _isBusy,
              initializing: _phase == _AppPhase.initializing,
              onStart: _runVerification,
              showReset: _phase == _AppPhase.done && _result != null,
              onReset: _reset,
            ),
          ],
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header();

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      color: _C.primary,
      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
      child: Row(
        children: [
          ClipRRect(
            borderRadius: BorderRadius.circular(12),
            child: Image.asset(
              'assets/branding/seon_app_icon.png',
              width: 48,
              height: 48,
              fit: BoxFit.cover,
              filterQuality: FilterQuality.high,
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'SEON Orchestration SDK',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 17,
                    fontWeight: FontWeight.w700,
                    letterSpacing: -0.3,
                  ),
                ),
                const SizedBox(height: 1),
                Text(
                  'Flutter SDK Example',
                  style: TextStyle(color: Colors.white.withValues(alpha: 0.65), fontSize: 12),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ConfigurationCard extends StatelessWidget {
  final SeonRegion region;
  final void Function(SeonRegion)? onRegion;
  final String baseUrlHint;
  final TextEditingController tokenController;
  final TextEditingController languageController;
  final bool inputsEnabled;

  const _ConfigurationCard({
    required this.region,
    required this.onRegion,
    required this.baseUrlHint,
    required this.tokenController,
    required this.languageController,
    required this.inputsEnabled,
  });

  @override
  Widget build(BuildContext context) {
    return _Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Configuration', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _C.text)),
          const SizedBox(height: 16),
          const Text('Region', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _C.text)),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: SeonRegion.values.map((r) {
              final selected = region == r;
              final label = r.name.toUpperCase();
              return ChoiceChip(
                label: Text(label),
                selected: selected,
                onSelected: onRegion == null ? null : (_) => onRegion!(r),
                selectedColor: _C.primary,
                labelStyle: TextStyle(
                  fontSize: 13,
                  fontWeight: FontWeight.w600,
                  color: selected ? Colors.white : _C.subtext,
                ),
                side: BorderSide(color: selected ? _C.primary : _C.border, width: 1.5),
                backgroundColor: _C.background,
                showCheckmark: false,
              );
            }).toList(),
          ),
          const SizedBox(height: 6),
          Text(baseUrlHint, style: const TextStyle(fontSize: 11, color: _C.subtext, height: 1.45)),
          const SizedBox(height: 16),
          const Row(
            children: [
              Text('Session Token ', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _C.text)),
              Text('*', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Color(0xFFEF4444))),
            ],
          ),
          const SizedBox(height: 8),
          TextField(
            controller: tokenController,
            enabled: inputsEnabled,
            obscureText: true,
            autocorrect: false,
            enableSuggestions: false,
            textCapitalization: TextCapitalization.none,
            decoration: _inputDecoration('Paste your workflow token here'),
          ),
          const SizedBox(height: 5),
          const Text(
            'Obtain this token from your backend after authenticating the user\'s session.',
            style: TextStyle(fontSize: 11, color: _C.subtext, height: 1.45),
          ),
          const SizedBox(height: 16),
          const Text('Language', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _C.text)),
          const SizedBox(height: 8),
          TextField(
            controller: languageController,
            enabled: inputsEnabled,
            maxLength: 5,
            autocorrect: false,
            enableSuggestions: false,
            textCapitalization: TextCapitalization.none,
            decoration: _inputDecoration('en'),
          ),
          const SizedBox(height: 5),
          const Text(
            'ISO 639-1 code (e.g. en, de, fr). Defaults to device locale if empty.',
            style: TextStyle(fontSize: 11, color: _C.subtext, height: 1.45),
          ),
        ],
      ),
    );
  }

  static InputDecoration _inputDecoration(String hint) {
    return InputDecoration(
      hintText: hint,
      hintStyle: const TextStyle(color: _C.placeholder),
      filled: true,
      fillColor: _C.background,
      counterText: '',
      contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(10),
        borderSide: const BorderSide(color: _C.border, width: 1.5),
      ),
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(10),
        borderSide: const BorderSide(color: _C.border, width: 1.5),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(10),
        borderSide: const BorderSide(color: _C.primary, width: 1.5),
      ),
    );
  }
}

class _StatusCard extends StatelessWidget {
  final bool isBusy;
  final String statusMessage;
  final Color? metaColor;

  const _StatusCard({
    required this.isBusy,
    required this.statusMessage,
    required this.metaColor,
  });

  @override
  Widget build(BuildContext context) {
    return _Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('Status', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _C.text)),
          const SizedBox(height: 12),
          Row(
            children: [
              if (isBusy)
                const SizedBox(
                  width: 20,
                  height: 20,
                  child: CircularProgressIndicator(strokeWidth: 2, color: _C.primary),
                )
              else
                Container(
                  width: 10,
                  height: 10,
                  decoration: BoxDecoration(
                    color: metaColor ?? _C.neutral,
                    shape: BoxShape.circle,
                  ),
                ),
              const SizedBox(width: 10),
              Expanded(
                child: Text(
                  statusMessage,
                  style: const TextStyle(fontSize: 14, color: _C.text, height: 1.4),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

class _ResultCard extends StatelessWidget {
  final SeonStatusMeta meta;
  final SeonVerificationResult result;
  final DateTime at;
  final String platformLine;

  const _ResultCard({
    required this.meta,
    required this.result,
    required this.at,
    required this.platformLine,
  });

  @override
  Widget build(BuildContext context) {
    final t = TimeOfDay.fromDateTime(at).format(context);
    return _Card(
      leftAccent: meta.color,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                width: 40,
                height: 40,
                decoration: BoxDecoration(color: meta.color, shape: BoxShape.circle),
                alignment: Alignment.center,
                child: Text(meta.icon, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700)),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(meta.label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _C.text)),
                    const SizedBox(height: 2),
                    Text(t, style: const TextStyle(fontSize: 12, color: _C.subtext)),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 16),
          _resultRow('Status', result.status.name),
          _resultRow('Platform', platformLine),
          if (result.errorMessage != null) _resultRow('Error', result.errorMessage!, valueColor: const Color(0xFFDC2626)),
        ],
      ),
    );
  }

  static Widget _resultRow(String key, String value, {Color? valueColor}) {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 8),
      decoration: const BoxDecoration(
        border: Border(top: BorderSide(color: _C.border)),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            flex: 1,
            child: Text(key, style: const TextStyle(fontSize: 12, color: _C.subtext, fontWeight: FontWeight.w500)),
          ),
          Expanded(
            flex: 2,
            child: Text(
              value,
              textAlign: TextAlign.right,
              style: TextStyle(fontSize: 12, color: valueColor ?? _C.text, fontWeight: FontWeight.w600),
            ),
          ),
        ],
      ),
    );
  }
}

class _InfoBanner extends StatelessWidget {
  const _InfoBanner();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: const Color(0xFFEFF6FF),
        borderRadius: BorderRadius.circular(12),
        border: Border.all(color: const Color(0xFFBFDBFE)),
      ),
      child: const Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Before you start', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: _C.primary)),
          SizedBox(height: 8),
          Text(
            '• Use a real physical device — the SDK detects simulators/emulators.\n'
            '• Grant camera, microphone, and location permissions when prompted.\n'
            '• Have a valid government-issued photo ID ready.\n'
            '• Ensure a stable internet connection before starting.',
            style: TextStyle(fontSize: 12, color: _C.primary, height: 1.55),
          ),
        ],
      ),
    );
  }
}

class _Footer extends StatelessWidget {
  final bool isBusy;
  final bool initializing;
  final VoidCallback onStart;
  final bool showReset;
  final VoidCallback onReset;

  const _Footer({
    required this.isBusy,
    required this.initializing,
    required this.onStart,
    required this.showReset,
    required this.onReset,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
      decoration: const BoxDecoration(
        color: _C.background,
        border: Border(top: BorderSide(color: _C.border)),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FilledButton(
            onPressed: isBusy ? null : onStart,
            style: FilledButton.styleFrom(
              minimumSize: const Size(double.infinity, 52),
              backgroundColor: isBusy ? _C.subtext : _C.primary,
              foregroundColor: Colors.white,
              padding: const EdgeInsets.symmetric(vertical: 16),
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
              elevation: isBusy ? 0 : 6,
              shadowColor: _C.primary.withValues(alpha: 0.35),
            ),
            child: isBusy
                ? Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      const SizedBox(
                        width: 18,
                        height: 18,
                        child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
                      ),
                      const SizedBox(width: 10),
                      Text(initializing ? 'Initializing…' : 'Verifying…', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
                    ],
                  )
                : const Text('Start Verification', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700)),
          ),
          if (showReset) ...[
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: onReset,
              style: OutlinedButton.styleFrom(
                minimumSize: const Size.fromHeight(48),
                side: const BorderSide(color: _C.border, width: 1.5),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
              ),
              child: const Text('Reset', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: _C.subtext)),
            ),
          ],
        ],
      ),
    );
  }
}

class _Card extends StatelessWidget {
  final Widget child;
  final Color? leftAccent;

  const _Card({required this.child, this.leftAccent});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      decoration: BoxDecoration(
        color: _C.card,
        borderRadius: BorderRadius.circular(16),
        border: leftAccent != null ? Border(left: BorderSide(color: leftAccent!, width: 4)) : null,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.06),
            offset: const Offset(0, 1),
            blurRadius: 6,
          ),
        ],
      ),
      padding: const EdgeInsets.all(20),
      child: child,
    );
  }
}
1
likes
150
points
348
downloads

Documentation

API reference

Publisher

verified publisherseon.io

Weekly Downloads

Flutter plugin wrapping the SEON Orchestration SDK for identity verification.

Homepage

License

MIT (license)

Dependencies

flutter

More

Packages that depend on seon_orchestration_flutter

Packages that implement seon_orchestration_flutter