import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/verification_session.dart'; import 'supabase_provider.dart'; /// Provider for the verification session controller. final verificationSessionControllerProvider = Provider((ref) { final client = ref.watch(supabaseClientProvider); return VerificationSessionController(client); }); /// Controller for creating, completing, and listening to verification sessions. class VerificationSessionController { VerificationSessionController(this._client); final SupabaseClient _client; /// Create a new verification session and return it. Future createSession({ required String type, String? contextId, }) async { final userId = _client.auth.currentUser!.id; final data = await _client .from('verification_sessions') .insert({'user_id': userId, 'type': type, 'context_id': contextId}) .select() .single(); return VerificationSession.fromMap(data); } /// Fetch a session by ID. Future getSession(String sessionId) async { final data = await _client .from('verification_sessions') .select() .eq('id', sessionId) .maybeSingle(); return data == null ? null : VerificationSession.fromMap(data); } /// Listen for realtime changes on a specific session (used by web to detect /// when mobile completes the verification). Stream watchSession(String sessionId) { return _client .from('verification_sessions') .stream(primaryKey: ['id']) .eq('id', sessionId) .map( (rows) => rows.isEmpty ? throw Exception('Session not found') : VerificationSession.fromMap(rows.first), ); } /// Complete a session: upload the face photo and mark the session as /// completed. Called from the mobile verification screen. Future completeSession({ required String sessionId, required Uint8List bytes, required String fileName, }) async { final userId = _client.auth.currentUser!.id; final ext = fileName.split('.').last.toLowerCase(); final path = '$userId/$sessionId.$ext'; // Upload to face-enrollment bucket (same bucket used for face photos) await _client.storage .from('face-enrollment') .uploadBinary( path, bytes, fileOptions: const FileOptions(upsert: true), ); final url = _client.storage.from('face-enrollment').getPublicUrl(path); // Mark session completed with the image URL await _client .from('verification_sessions') .update({'status': 'completed', 'image_url': url}) .eq('id', sessionId); } /// After a session completes, apply the result based on the session type. /// For 'enrollment': update the user's face photo. /// For 'verification': update the attendance log. Future applySessionResult(VerificationSession session) async { if (session.imageUrl == null) return; if (session.type == 'enrollment') { // Update profile face photo await _client .from('profiles') .update({ 'face_photo_url': session.imageUrl, 'face_enrolled_at': DateTime.now().toUtc().toIso8601String(), }) .eq('id', session.userId); } else if (session.type == 'verification' && session.contextId != null) { // Update attendance log verification status. // Cross-device verification defaults to check-in photo column. await _client .from('attendance_logs') .update({ 'verification_status': 'verified', 'check_in_verification_photo_url': session.imageUrl, }) .eq('id', session.contextId!); } } /// Expire a session (called when dialog is closed prematurely). Future expireSession(String sessionId) async { await _client .from('verification_sessions') .update({'status': 'expired'}) .eq('id', sessionId); } }