gyanmeet_flutter_sdk 1.6.0
gyanmeet_flutter_sdk: ^1.6.0 copied to clipboard
Gyanmeet meeting SDK for Flutter — a self-contained video meeting widget with host controls, chat, polls, batch exams, certificate verification, and optional on-device AI proctoring.
example/lib/main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:gyanmeet_flutter_sdk/gyanmeet_flutter_sdk.dart';
import 'class_join_tab.dart';
import 'dev_join_tab.dart';
import 'register_tab.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Gyanmeet Flutter SDK Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF366D5A)),
),
home: const HomeScreen(),
);
}
}
/// Dev harness for the SDK.
///
/// Four ways in, all sharing one backend URL:
/// • Dev join — mint a token straight from a room code with an API key.
/// • Register — the applicant registration form, which hands back a claim code.
/// • Class join — passport + claim code, the way a real applicant joins.
/// • Certificate — public verification, no session at all.
///
/// The API key path is dev-only: in production the consumer's backend mints the
/// token and the app never holds a key.
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _backend = TextEditingController(
text: 'https://israel-api.gyanmeet.com/api',
);
// The applicant routes resolve the organization from the request, not from
// the body. A browser supplies it via its Origin header because the web app
// is served from an org subdomain; a native app has none, so it is sent
// explicitly as X-Organization-Subdomain.
final _orgSubdomain = TextEditingController(text: 'israel');
String? _token;
// Reference image for the identity gate. Loaded from assets for testing; in
// production the consumer fetches it (backend photo, etc.). Optional — with
// none, the gate enrolls from the selfie taken at join.
Uint8List? _referenceImage;
@override
void initState() {
super.initState();
_loadReference();
}
// Set when the reference asset is absent. The gate still runs — with no
// reference it enrolls from the selfie instead of verifying against a photo.
String? _referenceError;
Future<void> _loadReference() async {
try {
final data = await rootBundle.load('assets/reference/person_a.png');
if (mounted) {
setState(() {
_referenceImage = data.buffer.asUint8List();
_referenceError = null;
});
}
} catch (e) {
debugPrint('reference load failed: $e');
if (mounted) {
setState(
() => _referenceError =
'No reference photo at example/assets/reference/person_a.png — '
'the identity gate will enroll from the selfie instead of '
'verifying against a reference.',
);
}
}
}
@override
void dispose() {
_backend.dispose();
_orgSubdomain.dispose();
super.dispose();
}
void _onToken(String token) => setState(() => _token = token);
@override
Widget build(BuildContext context) {
if (_token != null) return _meeting(_token!);
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('Gyanmeet SDK Example'),
bottom: const TabBar(
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: [
Tab(icon: Icon(Icons.bolt), text: 'Dev join'),
Tab(icon: Icon(Icons.how_to_reg), text: 'Register'),
Tab(icon: Icon(Icons.login), text: 'Class join'),
Tab(icon: Icon(Icons.verified), text: 'Certificate'),
],
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Column(
children: [
TextField(
controller: _backend,
decoration: const InputDecoration(
labelText: 'Backend URL',
border: OutlineInputBorder(),
),
// Every tab reads this controller on submit, so a rebuild
// is enough to keep the certificate tab's client in step.
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 8),
TextField(
controller: _orgSubdomain,
decoration: const InputDecoration(
labelText: 'Organization subdomain',
helperText:
'Used by Register and Class join. Leave blank only '
'if the backend sets PILOT_ORGANIZATION_ID.',
border: OutlineInputBorder(),
),
),
],
),
),
if (_referenceError != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.warning_amber, color: Colors.orange),
const SizedBox(width: 8),
Expanded(
child: Text(
_referenceError!,
style: const TextStyle(fontSize: 12),
),
),
],
),
),
),
Expanded(
child: TabBarView(
children: [
DevJoinTab(backend: _backend, onToken: _onToken),
RegisterTab(backend: _backend, orgSubdomain: _orgSubdomain),
ClassJoinTab(
backend: _backend,
orgSubdomain: _orgSubdomain,
onToken: _onToken,
),
GyanmeetCertificateVerification(
backendUrl: _backend.text.trim(),
primaryColor: '#366D5A',
),
],
),
),
],
),
),
);
}
Widget _meeting(String token) {
return GyanmeetMeeting(
backendUrl: _backend.text.trim(),
token: token,
config: const GyanmeetConfig(
videoMode: VideoMode.hostsOnly,
showPreJoin: true,
enableIdentityVerificationGate: true,
darkMode: true,
showParticipantsTab: false,
showPolls: false,
enableBreak: true,
enableExam: true,
// faceComparisonInterval: Duration(seconds: 10),
),
identityReferenceImage: _referenceImage,
primaryColor: '#366D5A',
onJoined: (id, role) => debugPrint('joined $id as $role'),
onLeave: () => setState(() => _token = null),
onEnded: () => setState(() => _token = null),
onError: (e) => debugPrint('error: $e'),
onFacePresenceChanged: (isPresent) =>
debugPrint('Face present: $isPresent'),
onGazeViolation: (reason) => debugPrint('Gaze violation: $reason'),
onIdentityMismatch: (similarity) =>
debugPrint('Identity mismatch! similarity=$similarity'),
onProctoringViolation: (v) => debugPrint(
'PROCTORING VIOLATION: ${v.type} reason=${v.reason} sim=${v.similarity}',
),
// onAiViolationWarning / onAiViolationRemoved are deliberately left
// unset: the SDK then draws the warning pill and the removal notice
// itself, which is what a consumer with no proctoring UI of its own gets.
);
}
}