mobile_proof_plugin 0.2.0
mobile_proof_plugin: ^0.2.0 copied to clipboard
Flutter plugin for TLSN-based mobile proofs with proxy/MPC auto-fallback, WebView capture, and custom provider flows.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_proof_plugin/mobile_proof_plugin.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'mobile_proof_plugin example',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF2563EB),
useMaterial3: true,
),
home: const ExampleHomePage(),
);
}
}
class ExampleHomePage extends StatefulWidget {
const ExampleHomePage({super.key});
@override
State<ExampleHomePage> createState() => _ExampleHomePageState();
}
class _ExampleHomePageState extends State<ExampleHomePage> {
static const String _providerRegistryAssetPath =
'packages/mobile_proof_plugin/assets/providers.json';
final TextEditingController _verifierUrlController = TextEditingController(
text: 'http://10.0.2.2:7047',
);
bool _running = false;
String _status = 'Ready';
ProofMode _proofMode = ProofMode.auto;
/// Set after a successful run: the mode that actually produced the proof,
/// derived from `artifact.payload['proof']['mode']`.
String? _completedMode;
/// When fallback occurred, the per-attempt timeline derived from
/// `artifact.payload['modeAttempts']`. Each entry has mode/result/durationMs
/// and possibly errorCode.
List<Map<String, Object?>>? _modeAttempts;
@override
void dispose() {
_verifierUrlController.dispose();
super.dispose();
}
Future<void> _runProof() async {
final verifierUrl = Uri.tryParse(_verifierUrlController.text.trim());
if (verifierUrl == null) {
setState(() {
_status = 'Invalid verifier URL';
});
return;
}
setState(() {
_running = true;
_status = 'Starting provider flow...';
_completedMode = null;
_modeAttempts = null;
});
final client = MobileProofClient(bridge: MethodChannelNativeBridge());
final StreamSubscription<SessionProgress> progressSub =
client.subscribeProgress().listen((progress) {
if (!mounted) return;
setState(() {
_status = '[${progress.phase.name}] ${progress.message}';
});
});
try {
final artifact = await client.attestProvider(
context: context,
providerId: 'kaggle.current_user.v1',
providerRegistryAssetPath: _providerRegistryAssetPath,
proofMode: _proofMode,
transportConfig: TransportConfig(
deploymentMode: DeploymentMode.hosted,
verifierUrl: verifierUrl,
trustedNotaryKeys: const <String>[
// Replace with production notary keys.
'REPLACE_WITH_BASE64_NOTARY_PUBLIC_KEY'
],
enforceNativeCore: false,
enableLogging: true,
includeProofTelemetry: true,
),
);
if (!mounted) return;
final proofMode = artifact.proofMode?.name;
final attemptsRaw = artifact.payload['modeAttempts'];
setState(() {
_completedMode = proofMode;
_modeAttempts = (attemptsRaw is List)
? attemptsRaw
.whereType<Map<String, Object?>>()
.toList(growable: false)
: null;
_status = 'Proof generated successfully';
});
} on ProofException catch (error) {
if (!mounted) return;
setState(() {
_status = 'Proof failed: [${error.code.name}] ${error.message}';
});
} finally {
await progressSub.cancel();
await client.dispose();
if (mounted) {
setState(() {
_running = false;
});
}
}
}
String _formatAttempts() {
final attempts = _modeAttempts;
if (attempts == null || attempts.isEmpty) return '';
return attempts.map((a) {
final mode = a['mode']?.toString() ?? '?';
final result = a['result']?.toString() ?? '?';
final ms = a['durationMs'];
final code = a['errorCode']?.toString();
final tail = code != null ? ' ($code)' : '';
return '$mode: $result, ${ms}ms$tail';
}).join(' → ');
}
@override
Widget build(BuildContext context) {
final completedMode = _completedMode;
final attemptsLine = _formatAttempts();
return Scaffold(
appBar: AppBar(title: const Text('mobile_proof_plugin')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _verifierUrlController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Verifier URL',
),
),
const SizedBox(height: 12),
Row(
children: <Widget>[
const Text('Proof mode:'),
const SizedBox(width: 12),
DropdownButton<ProofMode>(
value: _proofMode,
onChanged: _running
? null
: (m) {
if (m == null) return;
setState(() => _proofMode = m);
},
items: const <DropdownMenuItem<ProofMode>>[
DropdownMenuItem(
value: ProofMode.auto,
child: Text('Auto (proxy → mpc fallback)'),
),
DropdownMenuItem(
value: ProofMode.proxy,
child: Text('Proxy (no fallback)'),
),
DropdownMenuItem(
value: ProofMode.mpc,
child: Text('MPC (no fallback)'),
),
],
),
],
),
const SizedBox(height: 12),
FilledButton(
onPressed: _running ? null : _runProof,
child: Text(_running ? 'Running...' : 'Run Provider Attestation'),
),
const SizedBox(height: 12),
Text('Status: $_status'),
if (completedMode != null) ...<Widget>[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Completed via: $completedMode',
style: Theme.of(context).textTheme.titleMedium,
),
if (attemptsLine.isNotEmpty) ...<Widget>[
const SizedBox(height: 6),
Text('Timeline: $attemptsLine'),
],
],
),
),
],
],
),
),
);
}
}