Add optional KYC flow and service-backed account limits
This commit is contained in:
1 parent
61ccfc1844
commit
3397f04ab7
38 files changed
+3443
-36
No files matched your search
@@ -0,0 +1,199 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:sxchnge_analytics/sxchnge_analytics.dart';
|
||||
|
||||
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
||||
|
||||
import 'package:sxchnge_app/main.dart';
|
||||
import 'package:sxchnge_app/providers/locale.dart';
|
||||
|
||||
class Photos implements PhotoService {
|
||||
Photos(this.bytes);
|
||||
final Uint8List bytes;
|
||||
KycError? error;
|
||||
@override
|
||||
Future<Photo?> pick() async {
|
||||
if (error != null) throw KycException(error!);
|
||||
return Photo(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tap(WidgetTester t, Finder finder) async {
|
||||
await t.ensureVisible(finder);
|
||||
await t.tap(finder);
|
||||
await t.pumpAndSettle();
|
||||
}
|
||||
|
||||
void main() {
|
||||
late Uint8List bytes;
|
||||
setUpAll(() async {
|
||||
final r = ui.PictureRecorder();
|
||||
final c = ui.Canvas(r);
|
||||
c.drawRect(const Rect.fromLTWH(0, 0, 600, 600), Paint());
|
||||
final pic = r.endRecording();
|
||||
final image = await pic.toImage(600, 600);
|
||||
bytes = (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer
|
||||
.asUint8List();
|
||||
image.dispose();
|
||||
pic.dispose();
|
||||
});
|
||||
Future<DummyIdentityService> open(
|
||||
WidgetTester t, {
|
||||
Photos? photos,
|
||||
Locale? locale,
|
||||
}) async {
|
||||
t.platformDispatcher.defaultRouteNameTestValue = '/identity';
|
||||
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
|
||||
final service = DummyIdentityService(delay: Duration.zero);
|
||||
final provider = LocaleProvider();
|
||||
if (locale != null) await provider.setLocale(locale);
|
||||
await t.pumpWidget(
|
||||
SxchngeApp(
|
||||
identityService: service,
|
||||
photoService: photos ?? Photos(bytes),
|
||||
localeProvider: provider,
|
||||
),
|
||||
);
|
||||
await t.pumpAndSettle();
|
||||
return service;
|
||||
}
|
||||
|
||||
Future<void> submit(WidgetTester t) async {
|
||||
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
||||
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
||||
await tap(t, find.byKey(const ValueKey('kyc-next')));
|
||||
await tap(t, find.byKey(const ValueKey('pick-selfie')));
|
||||
await tap(t, find.byKey(const ValueKey('kyc-next')));
|
||||
await tap(t, find.byType(CheckboxListTile));
|
||||
await tap(t, find.byKey(const ValueKey('kyc-submit')));
|
||||
}
|
||||
|
||||
testWidgets('two photos, review, pending and approval with service limits', (
|
||||
t,
|
||||
) async {
|
||||
await open(t);
|
||||
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
||||
await submit(t);
|
||||
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
||||
await tap(t, find.byType(ExpansionTile));
|
||||
await tap(t, find.text('Approve'));
|
||||
expect(find.byKey(const ValueKey('kyc-approved')), findsOneWidget);
|
||||
expect(find.text('Verified'), findsOneWidget);
|
||||
expect(t.takeException(), null);
|
||||
});
|
||||
testWidgets('denial provides explanation and support, no resubmit', (
|
||||
t,
|
||||
) async {
|
||||
await open(t);
|
||||
await submit(t);
|
||||
await tap(t, find.byType(ExpansionTile));
|
||||
await tap(t, find.text('Decline'));
|
||||
expect(find.byKey(const ValueKey('kyc-denied')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('kyc-start')), findsNothing);
|
||||
await tap(t, find.text('Contact support'));
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
});
|
||||
testWidgets('correctable rejection can be resubmitted', (t) async {
|
||||
await open(t);
|
||||
await submit(t);
|
||||
await tap(t, find.byType(ExpansionTile));
|
||||
await tap(t, find.text('Request clearer passport'));
|
||||
expect(find.byKey(const ValueKey('kyc-changesRequested')), findsOneWidget);
|
||||
await submit(t);
|
||||
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
||||
});
|
||||
testWidgets('picker error recovery and discard confirmation', (t) async {
|
||||
final photos = Photos(bytes)..error = KycError.permission;
|
||||
await open(t, photos: photos);
|
||||
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
||||
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
||||
expect(find.byKey(const ValueKey('kyc-error')), findsOneWidget);
|
||||
photos.error = null;
|
||||
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
||||
await tap(t, find.text('Keep basic limits'));
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
await tap(t, find.text('Keep editing'));
|
||||
expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget);
|
||||
await tap(t, find.text('Keep basic limits'));
|
||||
await tap(t, find.text('Discard & leave'));
|
||||
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
||||
});
|
||||
testWidgets(
|
||||
'320px Russian at large text works through submission and keyboard',
|
||||
(t) async {
|
||||
t.view.physicalSize = const Size(320, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
t.platformDispatcher.textScaleFactorTestValue = 1.6;
|
||||
addTearDown(t.view.resetPhysicalSize);
|
||||
addTearDown(t.view.resetDevicePixelRatio);
|
||||
addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue);
|
||||
await open(t, locale: const Locale('ru'));
|
||||
expect(find.text('Верификация и лимиты'), findsOneWidget);
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await t.pump();
|
||||
await submit(t);
|
||||
expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget);
|
||||
expect(t.takeException(), null);
|
||||
},
|
||||
);
|
||||
testWidgets('status failure exposes retry without asserting basic state', (
|
||||
t,
|
||||
) async {
|
||||
t.platformDispatcher.defaultRouteNameTestValue = '/identity';
|
||||
addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue);
|
||||
final service = DummyIdentityService(delay: Duration.zero)
|
||||
..nextError = KycError.unavailable;
|
||||
await t.pumpWidget(
|
||||
SxchngeApp(
|
||||
identityService: service,
|
||||
photoService: Photos(bytes),
|
||||
analyticsService: DummyAnalyticsService(),
|
||||
),
|
||||
);
|
||||
await t.pumpAndSettle();
|
||||
expect(find.byKey(const ValueKey('kyc-unverified')), findsNothing);
|
||||
await tap(t, find.text('Refresh status & limits'));
|
||||
expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget);
|
||||
});
|
||||
testWidgets('route guard preserves draft when navigation is cancelled', (
|
||||
t,
|
||||
) async {
|
||||
await open(t);
|
||||
await tap(t, find.byKey(const ValueKey('kyc-start')));
|
||||
await tap(t, find.byKey(const ValueKey('pick-passport')));
|
||||
final router = GoRouter.of(t.element(find.text('Verification & limits')));
|
||||
router.go('/');
|
||||
await t.pumpAndSettle();
|
||||
expect(find.byType(AlertDialog), findsOneWidget);
|
||||
await tap(t, find.text('Keep editing'));
|
||||
expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget);
|
||||
router.go('/');
|
||||
await t.pumpAndSettle();
|
||||
await tap(t, find.text('Discard & leave'));
|
||||
expect(find.byKey(const ValueKey('kyc-start')), findsNothing);
|
||||
expect(find.text('Explore verification & limits'), findsOneWidget);
|
||||
});
|
||||
testWidgets('overview controls have accessible labels and touch targets', (
|
||||
t,
|
||||
) async {
|
||||
final semantics = t.ensureSemantics();
|
||||
await open(t);
|
||||
expect(t, meetsGuideline(labeledTapTargetGuideline));
|
||||
expect(t, meetsGuideline(androidTapTargetGuideline));
|
||||
final start = find.byKey(const ValueKey('kyc-start'));
|
||||
await t.ensureVisible(start);
|
||||
final buttonContext = t.element(start);
|
||||
FocusScope.of(buttonContext).requestFocus(Focus.of(buttonContext));
|
||||
await t.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await t.pump();
|
||||
expect(t.takeException(), null);
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user