Frontend first draft
This commit is contained in:
16
frontend/pweb/lib/pages/2fa/error_message.dart
Normal file
16
frontend/pweb/lib/pages/2fa/error_message.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class ErrorMessage extends StatelessWidget {
|
||||
final String error;
|
||||
|
||||
const ErrorMessage({super.key, required this.error});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
error,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
35
frontend/pweb/lib/pages/2fa/input.dart
Normal file
35
frontend/pweb/lib/pages/2fa/input.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pin_code_fields/pin_code_fields.dart';
|
||||
|
||||
|
||||
class TwoFactorCodeInput extends StatelessWidget {
|
||||
final void Function(String) onCompleted;
|
||||
|
||||
const TwoFactorCodeInput({super.key, required this.onCompleted});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: PinCodeTextField(
|
||||
length: 6,
|
||||
appContext: context,
|
||||
autoFocus: true,
|
||||
keyboardType: TextInputType.number,
|
||||
animationType: AnimationType.fade,
|
||||
pinTheme: PinTheme(
|
||||
shape: PinCodeFieldShape.box,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
fieldHeight: 48,
|
||||
fieldWidth: 40,
|
||||
inactiveColor: Theme.of(context).dividerColor,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
selectedColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onCompleted: onCompleted,
|
||||
onChanged: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
61
frontend/pweb/lib/pages/2fa/page.dart
Normal file
61
frontend/pweb/lib/pages/2fa/page.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/pages/2fa/error_message.dart';
|
||||
import 'package:pweb/pages/2fa/input.dart';
|
||||
import 'package:pweb/pages/2fa/prompt.dart';
|
||||
import 'package:pweb/pages/2fa/resend.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:pweb/providers/two_factor.dart';
|
||||
|
||||
|
||||
|
||||
class TwoFactorCodePage extends StatelessWidget {
|
||||
final VoidCallback onVerificationSuccess;
|
||||
|
||||
const TwoFactorCodePage({
|
||||
super.key,
|
||||
required this.onVerificationSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<TwoFactorProvider>(
|
||||
builder: (context, provider, child) {
|
||||
if (provider.verificationSuccess) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
onVerificationSuccess();
|
||||
});
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const TwoFactorPromptText(),
|
||||
const SizedBox(height: 32),
|
||||
TwoFactorCodeInput(
|
||||
onCompleted: (code) => provider.submitCode(code),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (provider.isSubmitting)
|
||||
const Center(child: CircularProgressIndicator())
|
||||
else
|
||||
const ResendCodeButton(),
|
||||
if (provider.hasError) ...[
|
||||
const SizedBox(height: 12),
|
||||
ErrorMessage(error: AppLocalizations.of(context)!.twoFactorError),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
15
frontend/pweb/lib/pages/2fa/prompt.dart
Normal file
15
frontend/pweb/lib/pages/2fa/prompt.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class TwoFactorPromptText extends StatelessWidget {
|
||||
const TwoFactorPromptText({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(
|
||||
AppLocalizations.of(context)!.twoFactorPrompt,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
31
frontend/pweb/lib/pages/2fa/resend.dart
Normal file
31
frontend/pweb/lib/pages/2fa/resend.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||
|
||||
|
||||
class ResendCodeButton extends StatelessWidget {
|
||||
const ResendCodeButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Add resend logic
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(0, 0),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
alignment: Alignment.centerLeft,
|
||||
foregroundColor: theme.colorScheme.primary,
|
||||
textStyle: theme.textTheme.bodyMedium?.copyWith(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
child: Text(localizations.twoFactorResend),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user