✨ Solarpay sheet
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
export 'content/alert.native.dart'
|
||||
@ -11,9 +12,21 @@ void showSnackBar(
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message), action: action));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
action: action,
|
||||
margin:
|
||||
isWideScreen(context)
|
||||
? null
|
||||
: EdgeInsets.fromLTRB(
|
||||
15.0,
|
||||
5.0,
|
||||
15.0,
|
||||
MediaQuery.of(context).padding.bottom + 28,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void clearSnackBar(BuildContext context) {
|
||||
|
243
lib/widgets/payment/README.md
Normal file
243
lib/widgets/payment/README.md
Normal file
@ -0,0 +1,243 @@
|
||||
# Payment Overlay Widget
|
||||
|
||||
A reusable payment verification overlay that supports both 6-digit PIN input and biometric authentication for secure payment processing.
|
||||
|
||||
## Features
|
||||
|
||||
- **6-digit PIN Input**: Secure numeric PIN entry with automatic focus management
|
||||
- **Biometric Authentication**: Support for fingerprint and face recognition
|
||||
- **Order Summary**: Display payment details including amount, description, and remarks
|
||||
- **Integrated API Calls**: Automatically handles payment processing via `/orders/{orderId}/pay`
|
||||
- **Error Handling**: Comprehensive error handling with user-friendly messages
|
||||
- **Loading States**: Visual feedback during payment processing
|
||||
- **Responsive Design**: Adapts to different screen sizes and orientations
|
||||
- **Customizable**: Flexible callbacks and styling options
|
||||
- **Accessibility**: Screen reader support and proper focus management
|
||||
- **Localization**: Full i18n support with easy_localization
|
||||
|
||||
## Usage
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/wallet.dart';
|
||||
import 'package:solian/widgets/payment/payment_overlay.dart';
|
||||
|
||||
// Create an order
|
||||
final order = SnWalletOrder(
|
||||
id: 'order_123',
|
||||
amount: 2500, // $25.00 in cents
|
||||
currency: 'USD',
|
||||
description: 'Premium Subscription',
|
||||
remarks: 'Monthly billing',
|
||||
status: 'pending',
|
||||
);
|
||||
|
||||
// Show payment overlay
|
||||
PaymentOverlay.show(
|
||||
context: context,
|
||||
order: order,
|
||||
onPaymentSuccess: (completedOrder) {
|
||||
// Handle successful payment
|
||||
print('Payment completed: ${completedOrder.id}');
|
||||
// Navigate to success page or update UI
|
||||
},
|
||||
onPaymentError: (error) {
|
||||
// Handle payment error
|
||||
print('Payment failed: $error');
|
||||
// Show error message to user
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.of(context).pop();
|
||||
print('Payment cancelled');
|
||||
},
|
||||
enableBiometric: true,
|
||||
);
|
||||
```
|
||||
|
||||
### Advanced Usage with Loading States
|
||||
|
||||
```dart
|
||||
bool isLoading = false;
|
||||
|
||||
PaymentOverlay.show(
|
||||
context: context,
|
||||
order: order,
|
||||
enableBiometric: true,
|
||||
isLoading: isLoading,
|
||||
onPinSubmit: (String pin) async {
|
||||
setState(() => isLoading = true);
|
||||
try {
|
||||
await processPaymentWithPin(pin);
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
showErrorDialog(e.toString());
|
||||
} finally {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
},
|
||||
onBiometricAuth: () async {
|
||||
setState(() => isLoading = true);
|
||||
try {
|
||||
final authenticated = await authenticateWithBiometrics();
|
||||
if (authenticated) {
|
||||
await processPaymentWithBiometrics();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorDialog(e.toString());
|
||||
} finally {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
### PaymentOverlay.show()
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `context` | `BuildContext` | ✅ | The build context for showing the overlay |
|
||||
| `order` | `SnWalletOrder` | ✅ | The order to be paid |
|
||||
| `onPaymentSuccess` | `Function(SnWalletOrder)?` | ❌ | Callback when payment succeeds with completed order |
|
||||
| `onPaymentError` | `Function(String)?` | ❌ | Callback when payment fails with error message |
|
||||
| `onCancel` | `VoidCallback?` | ❌ | Callback when payment is cancelled |
|
||||
| `enableBiometric` | `bool` | ❌ | Whether to show biometric option (default: true) |
|
||||
|
||||
## API Integration
|
||||
|
||||
The PaymentOverlay automatically handles payment processing by calling the `/orders/{orderId}/pay` endpoint with the following request body:
|
||||
|
||||
### PIN Payment
|
||||
```json
|
||||
{
|
||||
"pin": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
### Biometric Payment
|
||||
```json
|
||||
{
|
||||
"biometric": true
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
The API should return the completed `SnWalletOrder` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "order_123",
|
||||
"amount": 2500,
|
||||
"currency": "USD",
|
||||
"description": "Premium Subscription",
|
||||
"status": "completed",
|
||||
"processorReference": "txn_abc123",
|
||||
// ... other order fields
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
The widget handles common HTTP status codes:
|
||||
- `401`: Invalid PIN or biometric authentication failed
|
||||
- `400`: Bad request with custom error message
|
||||
- Other errors: Generic payment failed message
|
||||
|
||||
### Implementation Example
|
||||
|
||||
```dart
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
class BiometricService {
|
||||
final LocalAuthentication _auth = LocalAuthentication();
|
||||
|
||||
Future<bool> isAvailable() async {
|
||||
final isAvailable = await _auth.canCheckBiometrics;
|
||||
final isDeviceSupported = await _auth.isDeviceSupported();
|
||||
return isAvailable && isDeviceSupported;
|
||||
}
|
||||
|
||||
Future<bool> authenticate() async {
|
||||
try {
|
||||
final bool didAuthenticate = await _auth.authenticate(
|
||||
localizedReason: 'Please authenticate to complete payment',
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
stickyAuth: true,
|
||||
),
|
||||
);
|
||||
return didAuthenticate;
|
||||
} catch (e) {
|
||||
print('Biometric authentication error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Localization
|
||||
|
||||
Add these keys to your localization files:
|
||||
|
||||
```json
|
||||
{
|
||||
"paymentVerification": "Payment Verification",
|
||||
"paymentSummary": "Payment Summary",
|
||||
"amount": "Amount",
|
||||
"description": "Description",
|
||||
"pinCode": "PIN Code",
|
||||
"biometric": "Biometric",
|
||||
"enterPinToConfirm": "Enter your 6-digit PIN to confirm payment",
|
||||
"clearPin": "Clear PIN",
|
||||
"useBiometricToConfirm": "Use biometric authentication to confirm payment",
|
||||
"touchSensorToAuthenticate": "Touch the sensor to authenticate",
|
||||
"authenticating": "Authenticating...",
|
||||
"authenticateNow": "Authenticate Now",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"paymentFailed": "Payment failed. Please try again.",
|
||||
"invalidPin": "Invalid PIN. Please try again.",
|
||||
"biometricAuthFailed": "Biometric authentication failed. Please try again.",
|
||||
"paymentSuccess": "Payment completed successfully!",
|
||||
"paymentError": "Payment failed: {error}"
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The widget automatically adapts to your app's theme. It uses:
|
||||
|
||||
- `Theme.of(context).colorScheme.primary` for primary elements
|
||||
- `Theme.of(context).colorScheme.surface` for backgrounds
|
||||
- `Theme.of(context).textTheme` for typography
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **PIN Handling**: The PIN is passed as a string to your callback. Ensure you handle it securely and don't log it.
|
||||
2. **Biometric Authentication**: Always verify biometric authentication on your backend.
|
||||
3. **Network Security**: Use HTTPS for all payment-related API calls.
|
||||
4. **Data Validation**: Validate all payment data on your backend before processing.
|
||||
|
||||
## Example Integration
|
||||
|
||||
See `payment_overlay_example.dart` for a complete working example that demonstrates:
|
||||
|
||||
- How to show the overlay
|
||||
- Handling PIN and biometric authentication
|
||||
- Processing payments
|
||||
- Error handling
|
||||
- Loading states
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `flutter/material.dart` - Material Design components
|
||||
- `flutter/services.dart` - Input formatters and system services
|
||||
- `flutter_riverpod/flutter_riverpod.dart` - State management and dependency injection
|
||||
- `gap/gap.dart` - Spacing widgets
|
||||
- `material_symbols_icons/symbols.dart` - Material Symbols icons
|
||||
- `easy_localization/easy_localization.dart` - Internationalization
|
||||
- `dio/dio.dart` - HTTP client for API calls
|
||||
- `solian/models/wallet.dart` - Wallet order model
|
||||
- `solian/widgets/common/sheet_scaffold.dart` - Sheet scaffold widget
|
||||
- `solian/pods/network.dart` - API client provider
|
495
lib/widgets/payment/payment_overlay.dart
Normal file
495
lib/widgets/payment/payment_overlay.dart
Normal file
@ -0,0 +1,495 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:island/models/wallet.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class PaymentOverlay extends HookConsumerWidget {
|
||||
final SnWalletOrder order;
|
||||
final Function(SnWalletOrder completedOrder)? onPaymentSuccess;
|
||||
final Function(String error)? onPaymentError;
|
||||
final VoidCallback? onCancel;
|
||||
final bool enableBiometric;
|
||||
|
||||
const PaymentOverlay({
|
||||
super.key,
|
||||
required this.order,
|
||||
this.onPaymentSuccess,
|
||||
this.onPaymentError,
|
||||
this.onCancel,
|
||||
this.enableBiometric = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SheetScaffold(
|
||||
titleText: 'Solarpay',
|
||||
heightFactor: 0.7,
|
||||
child: _PaymentContent(
|
||||
order: order,
|
||||
onPaymentSuccess: onPaymentSuccess,
|
||||
onPaymentError: onPaymentError,
|
||||
onCancel: onCancel,
|
||||
enableBiometric: enableBiometric,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<SnWalletOrder?> show({
|
||||
required BuildContext context,
|
||||
required SnWalletOrder order,
|
||||
bool enableBiometric = true,
|
||||
}) {
|
||||
return showModalBottomSheet<SnWalletOrder>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
useSafeArea: true,
|
||||
builder:
|
||||
(context) => PaymentOverlay(
|
||||
order: order,
|
||||
enableBiometric: enableBiometric,
|
||||
onPaymentSuccess: (completedOrder) {
|
||||
Navigator.of(context).pop(completedOrder);
|
||||
},
|
||||
onPaymentError: (err) {
|
||||
Navigator.of(context).pop();
|
||||
showErrorAlert(err);
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentContent extends ConsumerStatefulWidget {
|
||||
final SnWalletOrder order;
|
||||
final Function(SnWalletOrder)? onPaymentSuccess;
|
||||
final Function(String)? onPaymentError;
|
||||
final VoidCallback? onCancel;
|
||||
final bool enableBiometric;
|
||||
|
||||
const _PaymentContent({
|
||||
required this.order,
|
||||
this.onPaymentSuccess,
|
||||
this.onPaymentError,
|
||||
this.onCancel,
|
||||
this.enableBiometric = true,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_PaymentContent> createState() => _PaymentContentState();
|
||||
}
|
||||
|
||||
class _PaymentContentState extends ConsumerState<_PaymentContent> {
|
||||
static const String _pinStorageKey = 'app_pin_code';
|
||||
static final _secureStorage = FlutterSecureStorage();
|
||||
|
||||
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||
|
||||
String _pin = '';
|
||||
bool _isPinMode = true;
|
||||
bool _hasBiometricSupport = false;
|
||||
bool _hasStoredPin = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeBiometric();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _initializeBiometric() async {
|
||||
try {
|
||||
// Check if biometric is available
|
||||
final isAvailable = await _localAuth.isDeviceSupported();
|
||||
final canCheckBiometrics = await _localAuth.canCheckBiometrics;
|
||||
_hasBiometricSupport = isAvailable && canCheckBiometrics;
|
||||
|
||||
// Check if PIN is stored
|
||||
final storedPin = await _secureStorage.read(key: _pinStorageKey);
|
||||
_hasStoredPin = storedPin != null && storedPin.isNotEmpty;
|
||||
|
||||
// Set initial mode based on stored PIN and biometric support
|
||||
if (_hasStoredPin && _hasBiometricSupport && widget.enableBiometric) {
|
||||
_isPinMode = false;
|
||||
// Automatically trigger biometric authentication
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_authenticateWithBiometric();
|
||||
});
|
||||
} else {
|
||||
_isPinMode = true;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to PIN mode if biometric setup fails
|
||||
_isPinMode = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onPinSubmit(String pin) {
|
||||
_pin = pin;
|
||||
if (pin.length == 6) {
|
||||
_processPaymentWithPin(pin);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _processPaymentWithPin(String pin) async {
|
||||
showLoadingModal(context);
|
||||
|
||||
try {
|
||||
// Store PIN securely for future biometric authentication
|
||||
if (_hasBiometricSupport && widget.enableBiometric && !_hasStoredPin) {
|
||||
await _secureStorage.write(key: _pinStorageKey, value: pin);
|
||||
_hasStoredPin = true;
|
||||
}
|
||||
|
||||
await _makePaymentRequest(pin);
|
||||
} catch (err) {
|
||||
widget.onPaymentError?.call(err.toString());
|
||||
_pin = '';
|
||||
} finally {
|
||||
if (mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _authenticateWithBiometric() async {
|
||||
showLoadingModal(context);
|
||||
|
||||
try {
|
||||
// Perform biometric authentication
|
||||
final bool didAuthenticate = await _localAuth.authenticate(
|
||||
localizedReason: 'biometricPrompt'.tr(),
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
stickyAuth: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (didAuthenticate) {
|
||||
// Retrieve stored PIN and process payment
|
||||
final storedPin = await _secureStorage.read(key: _pinStorageKey);
|
||||
if (storedPin != null && storedPin.isNotEmpty) {
|
||||
await _makePaymentRequest(storedPin);
|
||||
} else {
|
||||
// Fallback to PIN mode if no stored PIN
|
||||
_fallbackToPinMode('noStoredPin'.tr());
|
||||
}
|
||||
} else {
|
||||
// Biometric authentication failed, fallback to PIN mode
|
||||
_fallbackToPinMode('biometricAuthFailed'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
// Handle biometric authentication errors
|
||||
String errorMessage = 'biometricAuthFailed'.tr();
|
||||
if (err is PlatformException) {
|
||||
switch (err.code) {
|
||||
case 'NotAvailable':
|
||||
errorMessage = 'biometricNotAvailable'.tr();
|
||||
break;
|
||||
case 'NotEnrolled':
|
||||
errorMessage = 'biometricNotEnrolled'.tr();
|
||||
break;
|
||||
case 'LockedOut':
|
||||
case 'PermanentlyLockedOut':
|
||||
errorMessage = 'biometricLockedOut'.tr();
|
||||
break;
|
||||
default:
|
||||
errorMessage = 'biometricAuthFailed'.tr();
|
||||
}
|
||||
}
|
||||
_fallbackToPinMode(errorMessage);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unified method for making payment requests with PIN
|
||||
Future<void> _makePaymentRequest(String pin) async {
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.post(
|
||||
'/orders/${widget.order.id}/pay',
|
||||
data: {'pin_code': pin},
|
||||
);
|
||||
|
||||
final completedOrder = SnWalletOrder.fromJson(response.data);
|
||||
widget.onPaymentSuccess?.call(completedOrder);
|
||||
} catch (err) {
|
||||
String errorMessage = 'paymentFailed'.tr();
|
||||
if (err is DioException) {
|
||||
if (err.response?.statusCode == 403 ||
|
||||
err.response?.statusCode == 401) {
|
||||
// PIN is invalid
|
||||
errorMessage = 'invalidPin'.tr();
|
||||
// If this was a biometric attempt with stored PIN, remove the stored PIN
|
||||
if (!_isPinMode) {
|
||||
await _secureStorage.delete(key: _pinStorageKey);
|
||||
_hasStoredPin = false;
|
||||
_fallbackToPinMode(errorMessage);
|
||||
return;
|
||||
}
|
||||
} else if (err.response?.statusCode == 400) {
|
||||
errorMessage = err.response?.data?['error'] ?? errorMessage;
|
||||
}
|
||||
}
|
||||
throw errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
void _fallbackToPinMode(String? message) {
|
||||
setState(() {
|
||||
_isPinMode = true;
|
||||
});
|
||||
if (message != null && message.isNotEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatCurrency(int amount, String currency) {
|
||||
final value = amount / 100.0;
|
||||
return '${value.toStringAsFixed(2)} $currency';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Order Summary
|
||||
_buildOrderSummary(),
|
||||
const Gap(32),
|
||||
|
||||
// Authentication Content
|
||||
Expanded(
|
||||
child: _isPinMode ? _buildPinInput() : _buildBiometricAuth(),
|
||||
),
|
||||
|
||||
// Action Buttons
|
||||
const Gap(24),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOrderSummary() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.receipt,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'paymentSummary'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'amount'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Text(
|
||||
_formatCurrency(widget.order.amount, widget.order.currency),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.order.remarks != null) ...[
|
||||
const Gap(8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'description'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
widget.order.remarks!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPinInput() {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'enterPinToConfirm'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(24),
|
||||
OtpTextField(
|
||||
numberOfFields: 6,
|
||||
borderColor: Theme.of(context).colorScheme.outline,
|
||||
focusedBorderColor: Theme.of(context).colorScheme.primary,
|
||||
showFieldAsBox: true,
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
fieldWidth: 48,
|
||||
fieldHeight: 56,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderWidth: 1,
|
||||
textStyle: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
onSubmit: _onPinSubmit,
|
||||
onCodeChanged: (String code) {
|
||||
_pin = code;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBiometricAuth() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Symbols.fingerprint,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Text(
|
||||
'useBiometricToConfirm'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
'touchSensorToAuthenticate'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _authenticateWithBiometric,
|
||||
icon: const Icon(Symbols.fingerprint),
|
||||
label: Text('authenticateNow'.tr()),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
TextButton(
|
||||
onPressed: () => _fallbackToPinMode(null),
|
||||
child: Text('usePinInstead'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onCancel,
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
),
|
||||
if (_isPinMode && _pin.length == 6) ...[
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => _processPaymentWithPin(_pin),
|
||||
child: Text('confirm'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user