239 lines
7.9 KiB
Dart
239 lines
7.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
|
|
|
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
|
|
|
import 'copy.dart';
|
|
import 'form.dart';
|
|
import 'limits.dart';
|
|
import 'status.dart';
|
|
|
|
import '../generated/i18n/app_localizations.dart';
|
|
|
|
class IdentityPage extends StatefulWidget {
|
|
const IdentityPage({super.key});
|
|
@override
|
|
State<IdentityPage> createState() => IdentityPageState();
|
|
}
|
|
|
|
class IdentityPageState extends State<IdentityPage> {
|
|
late final _limits = LimitsProvider(context.read<LimitsService>());
|
|
late final _identity = IdentityProvider(
|
|
context.read<IdentityService>(),
|
|
context.read<PhotoService>(),
|
|
JourneyRecorder(context.read<AnalyticsService>()),
|
|
onStatusChanged: _limits.refresh,
|
|
);
|
|
final _contentKey = GlobalKey();
|
|
final _feedbackKey = GlobalKey();
|
|
final _focus = FocusNode(skipTraversal: true);
|
|
String? _lastView;
|
|
void _transition() {
|
|
final p = _identity;
|
|
if (p.busy) return;
|
|
final view = '${p.editing}:${p.step}:${p.record?.status}:${p.error}';
|
|
if (_lastView == null) {
|
|
_lastView = view;
|
|
return;
|
|
}
|
|
if (view == _lastView) return;
|
|
_lastView = view;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
final target = p.error != null
|
|
? _feedbackKey.currentContext
|
|
: _contentKey.currentContext;
|
|
if (target != null) {
|
|
Scrollable.ensureVisible(
|
|
target,
|
|
alignment: 0.05,
|
|
duration: const Duration(milliseconds: 180),
|
|
);
|
|
}
|
|
if (p.error == null) _focus.requestFocus();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
_identity.addListener(_transition);
|
|
_identity.refresh();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<bool> allowExit() async {
|
|
if (_identity.busy) return false;
|
|
if (!_identity.hasDraft) return true;
|
|
final l = AppLocalizations.of(context)!;
|
|
return await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(l.kycDiscardTitle),
|
|
content: Text(l.kycDiscardBody),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: Text(l.kycStay),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: Text(l.kycDiscard),
|
|
),
|
|
],
|
|
),
|
|
) ??
|
|
false;
|
|
}
|
|
|
|
Future<void> _leave({bool account = false}) async {
|
|
if (!await allowExit() || !mounted) return;
|
|
_identity.exit();
|
|
if (account) {
|
|
if (context.canPop()) {
|
|
context.pop();
|
|
} else {
|
|
context.go('/limits');
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_identity.removeListener(_transition);
|
|
_identity.dispose();
|
|
_focus.dispose();
|
|
_limits.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ChangeNotifierProvider.value(
|
|
value: _limits,
|
|
child: ListenableBuilder(
|
|
listenable: _identity,
|
|
builder: (context, _) {
|
|
final p = _identity;
|
|
final l = AppLocalizations.of(context)!;
|
|
final t = Theme.of(context);
|
|
return PopScope(
|
|
canPop: !p.hasDraft && !p.busy,
|
|
onPopInvokedWithResult: (didPop, _) {
|
|
if (!didPop) _leave(account: true);
|
|
},
|
|
child: Scaffold(
|
|
body: SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 720),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const SizedBox(height: 36),
|
|
Semantics(
|
|
header: true,
|
|
child: Text(
|
|
l.kycTitle,
|
|
style: t.textTheme.displaySmall,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(l.kycIntro, style: t.textTheme.bodyLarge),
|
|
const SizedBox(height: 20),
|
|
if (p.isDemo)
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: t.colorScheme.tertiaryContainer,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Text(
|
|
l.kycDemo,
|
|
style: TextStyle(
|
|
color: t.colorScheme.onTertiaryContainer,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
if (p.busy) ...[
|
|
const LinearProgressIndicator(),
|
|
const SizedBox(height: 12),
|
|
Semantics(
|
|
liveRegion: true,
|
|
child: Text(
|
|
p.record == null ? l.kycLoading : l.kycWorking,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
if (p.error != null) ...[
|
|
Semantics(
|
|
key: _feedbackKey,
|
|
liveRegion: true,
|
|
child: Text(
|
|
errorText(l, p.error!),
|
|
key: const ValueKey('kyc-error'),
|
|
style: TextStyle(color: t.colorScheme.error),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
if (p.stale && p.record != null)
|
|
Text(
|
|
l.kycStatusStale,
|
|
style: TextStyle(color: t.colorScheme.error),
|
|
),
|
|
Focus(
|
|
includeSemantics: false,
|
|
key: _contentKey,
|
|
focusNode: _focus,
|
|
child: p.editing
|
|
? IdentityForm(provider: p)
|
|
: p.record != null
|
|
? StatusPanel(provider: p)
|
|
: const SizedBox.shrink(),
|
|
),
|
|
if (!p.editing || p.stale)
|
|
TextButton(
|
|
onPressed: p.busy ? null : p.refresh,
|
|
child: Text(l.kycRefresh),
|
|
),
|
|
if (!p.editing) ...[
|
|
const SizedBox(height: 16),
|
|
const LimitsPanel(),
|
|
],
|
|
const SizedBox(height: 16),
|
|
if (p.editing)
|
|
TextButton(
|
|
onPressed: p.busy ? null : _leave,
|
|
child: Text(l.kycSkip),
|
|
),
|
|
TextButton(
|
|
onPressed: p.busy
|
|
? null
|
|
: () => _leave(account: true),
|
|
child: Text(l.wLimits),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|