import 'package:flutter/material.dart'; import 'package:payloads_shared/payloads_shared.dart'; import 'package:payloads_app1/payloads_app1.dart'; import 'src/web_bridge.dart'; void main() { runApp(const WebApp1()); } class WebApp1 extends StatelessWidget { const WebApp1({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Web App 1 - Bridge Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true, ), home: const WebApp1Home(), ); } } class WebApp1Home extends StatefulWidget { const WebApp1Home({super.key}); @override State createState() => _WebApp1HomeState(); } class _WebApp1HomeState extends State { final WebBridge _webBridge = WebBridge(); final TextEditingController _quizIdController = TextEditingController(); String _status = 'Initializing...'; bool _isBridgeReady = false; // Events list for received payloads final List> _receivedPayloads = []; static const int _maxPayloads = 20; @override void initState() { super.initState(); _initializeApp(); } void _initializeApp() { // Register payload types (ONLY shared + app1, NOT host!) _registerPayloads(); // Initialize bridge _webBridge.initialize(); // Set up payload listener _webBridge.onPayloadReceived = (payload) { _addReceivedPayload('Received', payload.runtimeType.toString(), payload.toString()); }; // Check bridge status Future.delayed(const Duration(milliseconds: 500), () { setState(() { _isBridgeReady = true; // Bridge is always ready after initialization _status = _isBridgeReady ? 'Bridge ready' : 'Bridge not ready'; }); }); } void _registerPayloads() { // Register shared payloads registerSharedPayloads(); // Register app1-specific payloads registerApp1Payloads(); // Note: payloads_host is NOT registered - demonstrates isolation } void _addReceivedPayload(String type, String payloadType, String details) { setState(() { _receivedPayloads.insert(0, { 'timestamp': DateTime.now(), 'type': type, 'payloadType': payloadType, 'details': details, }); // Keep only last N payloads if (_receivedPayloads.length > _maxPayloads) { _receivedPayloads.removeRange(_maxPayloads, _receivedPayloads.length); } }); } void _sendPing() async { setState(() => _status = 'Sending ping...'); try { await _webBridge.sendPing('Hello from Web App 1!'); setState(() => _status = 'Ping sent successfully'); } catch (e) { setState(() => _status = 'Error sending ping: $e'); } } void _requestUserInfo() async { setState(() => _status = 'Requesting user info...'); try { await _webBridge.requestUserInfo(['userId', 'username', 'email', 'isAdmin']); setState(() => _status = 'User info request sent'); } catch (e) { setState(() => _status = 'Error requesting user info: $e'); } } void _sendQuizResults() async { final quizId = _quizIdController.text.trim(); if (quizId.isEmpty) { setState(() => _status = 'Please enter quiz ID'); return; } setState(() => _status = 'Sending quiz results...'); try { // Create sample quiz answers final answers = [ QuizAnswer( questionId: 'q1', selectedAnswer: 'Answer A', isCorrect: true, timeSpent: 30, ), QuizAnswer( questionId: 'q2', selectedAnswer: 'Answer B', isCorrect: false, timeSpent: 60, ), ]; await _webBridge.submitQuizResults( quizId: quizId, answers: answers, timeSpent: 135, ); setState(() => _status = 'Quiz results sent'); } catch (e) { setState(() => _status = 'Error sending quiz results: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text('Web App 1 - Bridge Demo'), actions: [ Icon( _isBridgeReady ? Icons.check_circle : Icons.error, color: _isBridgeReady ? Colors.green : Colors.red, ), const SizedBox(width: 8), ], ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Status card Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Status: $_status', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Text( 'Bridge: ${_isBridgeReady ? "Ready" : "Not Ready"}', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 8), Text( 'Payload Isolation: ✅ Only shared + app1 payloads available', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.green, ), ), ], ), ), ), const SizedBox(height: 16), // Test buttons ElevatedButton.icon( onPressed: _isBridgeReady ? _sendPing : null, icon: const Icon(Icons.send), label: const Text('Send Ping'), ), const SizedBox(height: 8), ElevatedButton.icon( onPressed: _isBridgeReady ? _requestUserInfo : null, icon: const Icon(Icons.person), label: const Text('Request User Info'), ), const SizedBox(height: 16), // Quiz section Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Quiz Test', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), TextField( controller: _quizIdController, decoration: const InputDecoration( labelText: 'Quiz ID', border: OutlineInputBorder(), hintText: 'e.g., quiz_123', ), ), const SizedBox(height: 8), ElevatedButton.icon( onPressed: _isBridgeReady ? _sendQuizResults : null, icon: const Icon(Icons.quiz), label: const Text('Submit Quiz'), ), ], ), ), ), const SizedBox(height: 16), // Received Payloads panel Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.download, size: 16), const SizedBox(width: 8), Text( 'Received Payloads (${_receivedPayloads.length})', style: Theme.of(context).textTheme.titleSmall, ), const Spacer(), TextButton( onPressed: () => setState(() => _receivedPayloads.clear()), child: const Text('Clear', style: TextStyle(fontSize: 12)), ), ], ), const SizedBox(height: 8), if (_receivedPayloads.isEmpty) Container( height: 100, decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), ), child: const Center( child: Text( 'No payloads received yet', style: TextStyle(color: Colors.grey), ), ), ) else SizedBox( height: 200, child: ListView.builder( itemCount: _receivedPayloads.length, itemBuilder: (context, index) { final payload = _receivedPayloads[index]; final timestamp = payload['timestamp'] as DateTime; final payloadType = payload['payloadType'] as String; final details = payload['details'] as String; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.download, size: 16, color: Colors.blue.shade700, ), const SizedBox(width: 8), Expanded( child: Text( payloadType, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: Colors.blue.shade800, ), ), ), Text( '${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}:${timestamp.second.toString().padLeft(2, '0')}', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey.shade600, ), ), ], ), const SizedBox(height: 4), Text( details, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey.shade700, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ], ), ); }, ), ), ], ), ), ), const SizedBox(height: 16), // Footer Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Text( 'Payload Isolation Demo', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), Text( 'This web app can only access shared and app1 payloads.', style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( 'Host-specific payloads are NOT available here.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.orange, ), textAlign: TextAlign.center, ), ], ), ), ), ], ), ), padding: const EdgeInsets.only(bottom: 32.0), ), ); } }