400 lines
14 KiB
Dart
400 lines
14 KiB
Dart
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<WebApp1Home> createState() => _WebApp1HomeState();
|
|
}
|
|
|
|
class _WebApp1HomeState extends State<WebApp1Home> {
|
|
final WebBridge _webBridge = WebBridge();
|
|
final TextEditingController _quizIdController = TextEditingController();
|
|
|
|
String _status = 'Initializing...';
|
|
bool _isBridgeReady = false;
|
|
|
|
// Events list for received payloads
|
|
final List<Map<String, dynamic>> _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),
|
|
),
|
|
);
|
|
}
|
|
}
|