zally_flutter_sdk 1.2.0
zally_flutter_sdk: ^1.2.0 copied to clipboard
Continuous Authentication for Flutter. Wraps the native zally iOS and Android SDKs behind one Dart API.
example/lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:zally_flutter_sdk/zally_flutter_sdk.dart';
/// Supply at run time so nothing secret is committed:
///
/// ```
/// flutter run --dart-define=ZALLY_APP_UUID=... --dart-define=ZALLY_TOKEN_URL=https://...
/// ```
///
/// ZALLY_TOKEN_URL is fetched with GET each time the SDK asks for a token. The
/// response body is the JWT, or JSON with a top-level `token` field.
const String _defaultAppUuid = String.fromEnvironment('ZALLY_APP_UUID');
const String _defaultTokenUrl = String.fromEnvironment('ZALLY_TOKEN_URL');
void main() {
runApp(const ZallyExampleApp());
}
class ZallyExampleApp extends StatelessWidget {
const ZallyExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'zally Flutter SDK example',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF1F3A5F),
useMaterial3: true,
),
home: const ZallyExampleScreen(),
);
}
}
class ZallyExampleScreen extends StatefulWidget {
const ZallyExampleScreen({super.key});
@override
State<ZallyExampleScreen> createState() => _ZallyExampleScreenState();
}
class _ZallyExampleScreenState extends State<ZallyExampleScreen> {
final ZallySdk _zally = ZallySdk.instance;
final TextEditingController _appUuid = TextEditingController(
text: _defaultAppUuid,
);
final TextEditingController _tokenUrl = TextEditingController(
text: _defaultTokenUrl,
);
final List<String> _log = <String>[];
StreamSubscription<UserState>? _stateSub;
UserState? _state;
bool _busy = false;
int _tokenRequests = 0;
@override
void initState() {
super.initState();
_stateSub = _zally.userStateStream.listen(
(UserState s) => setState(() => _state = s),
onError: (Object e) => _append('userStateStream error: $e'),
);
}
@override
void dispose() {
_stateSub?.cancel();
_appUuid.dispose();
_tokenUrl.dispose();
super.dispose();
}
void _append(String line) {
final now = DateTime.now();
final stamp =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
setState(() => _log.insert(0, '$stamp $line'));
}
/// The client token provider. The SDK calls this on initialize and again
/// whenever its refresh token expires. Each call is logged so the
/// re-invocation is visible on screen.
Future<String?> _getClientToken() async {
_tokenRequests++;
_append('requestClientToken #$_tokenRequests');
final url = _tokenUrl.text.trim();
if (url.isEmpty) {
_append(' no token URL set, answering null');
return null;
}
final client = HttpClient();
try {
final request = await client.getUrl(Uri.parse(url));
final response = await request.close();
final body = await response.transform(utf8.decoder).join();
if (response.statusCode != 200) {
_append(' token endpoint returned ${response.statusCode}');
return null;
}
final trimmed = body.trim();
if (trimmed.startsWith('{')) {
final decoded = jsonDecode(trimmed);
final token = decoded is Map ? decoded['token'] as String? : null;
_append(
token == null ? ' JSON body has no token field' : ' token received',
);
return token;
}
_append(' token received');
return trimmed;
} finally {
client.close();
}
}
Future<void> _run(String name, Future<Object?> Function() action) async {
setState(() => _busy = true);
_append('$name()');
try {
final result = await action();
_append(' ok${result == null ? '' : ': $result'}');
} on ZallyException catch (e) {
_append(
' ${e.code}${e.authReason == null ? '' : ' ${e.authReason!.name}'}: ${e.message}',
);
} catch (e) {
_append(' unexpected: $e');
} finally {
if (mounted) setState(() => _busy = false);
}
}
@override
Widget build(BuildContext context) {
final state = _state;
return Scaffold(
appBar: AppBar(title: const Text('zally Flutter SDK example')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _appUuid,
decoration: const InputDecoration(labelText: 'App UUID'),
),
const SizedBox(height: 8),
TextField(
controller: _tokenUrl,
decoration: const InputDecoration(
labelText: 'Token URL (GET, returns JWT)',
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: <Widget>[
FilledButton(
onPressed: _busy
? null
: () => _run(
'initialize',
() => _zally.initialize(
appUUID: _appUuid.text,
getClientToken: _getClientToken,
),
),
child: const Text('Initialize'),
),
FilledButton.tonal(
onPressed: _busy ? null : () => _run('start', _zally.start),
child: const Text('Start'),
),
FilledButton.tonal(
onPressed: _busy ? null : () => _run('stop', _zally.stop),
child: const Text('Stop'),
),
OutlinedButton(
onPressed: _busy
? null
: () => _run('isInitialized / isRunning', () async {
final init = await _zally.isInitialized();
final running = await _zally.isRunning();
return 'initialized=$init running=$running';
}),
child: const Text('Query'),
),
OutlinedButton(
onPressed: _busy
? null
: () => _run('getUserState', _zally.getUserState),
child: const Text('Refresh state'),
),
],
),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: state == null
? const Text('No user state received yet')
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('training: ${state.trainingStateRaw}'),
Text('zId: ${state.zIdStatusRaw}'),
Text(
'isUser: ${state.isUser} isImposter: ${state.isImposter}',
),
Text(
'isOutOfTraining: ${state.isOutOfTraining} isInactive: ${state.isInactive}',
),
],
),
),
),
const SizedBox(height: 12),
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
),
child: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _log.length,
itemBuilder: (BuildContext context, int i) => Text(
_log[i],
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
),
],
),
),
);
}
}