Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ plugins {

android {
namespace = "network.mostro.app"
compileSdk = 35 // flutter.compileSdkVersion
ndkVersion = "26.3.11579264" //flutter.ndkVersion
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

compileOptions {
coreLibraryDesugaringEnabled true
Expand All @@ -26,7 +26,7 @@ android {
applicationId = "network.mostro.app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 23 // flutter.minSdkVersion
minSdk = flutter.minSdkVersion // flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
Expand Down
3 changes: 1 addition & 2 deletions l10n.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ arb-dir: lib/l10n
template-arb-file: intl_en.arb
output-dir: lib/generated
output-localization-file: l10n.dart
output-class: S
synthetic-package: false
output-class: S
2 changes: 1 addition & 1 deletion lib/data/models/mostro_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class MostroMessage<T extends Payload> {
NostrKeyPairs? masterKey,
int? keyIndex,
}) async {
this.tradeIndex = keyIndex;
tradeIndex = keyIndex;
final content = serialize(keyPair: masterKey != null ? tradeKey : null);
final keySet = masterKey ?? tradeKey;

Expand Down
4 changes: 1 addition & 3 deletions lib/data/repositories/mostro_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ class MostroStorage extends BaseStorage<MostroMessage> {
if (await hasItem(id)) return;
// Add metadata for easier querying
final Map<String, dynamic> dbMap = message.toJson();
if (message.timestamp == null) {
message.timestamp = DateTime.now().millisecondsSinceEpoch;
}
message.timestamp ??= DateTime.now().millisecondsSinceEpoch;
dbMap['timestamp'] = message.timestamp;

await store.record(id).put(db, dbMap);
Expand Down
2 changes: 1 addition & 1 deletion lib/features/auth/screens/register_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class RegisterScreen extends HookConsumerWidget {
ref.read(useBiometricsProvider.notifier).state =
value;
},
activeColor: Colors.green,
activeThumbColor: AppTheme.activeColor,
)
: const SizedBox.shrink();
}),
Expand Down
9 changes: 5 additions & 4 deletions lib/features/key_manager/key_management_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
}

Future<void> _generateNewMasterKey() async {
final sessionNotifer = ref.read(sessionNotifierProvider.notifier);
await sessionNotifer.reset();
final sessionNotifier = ref.read(sessionNotifierProvider.notifier);
await sessionNotifier.reset();

final mostroStorage = ref.read(mostroStorageProvider);
await mostroStorage.deleteAll();
Expand Down Expand Up @@ -396,7 +396,7 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
.watch(settingsProvider.notifier)
.updatePrivacyMode(value);
},
activeColor: AppTheme.activeColor,
activeThumbColor: AppTheme.activeColor,
inactiveThumbColor: AppTheme.textSecondary,
inactiveTrackColor: AppTheme.backgroundInactive,
),
Expand Down Expand Up @@ -672,7 +672,8 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
child: Text(
S.of(context)!.continueButton,
Expand Down
170 changes: 159 additions & 11 deletions lib/features/order/screens/add_order_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import 'package:mostro_mobile/features/order/widgets/premium_section.dart';
import 'package:mostro_mobile/features/order/widgets/price_type_section.dart';
import 'package:mostro_mobile/features/order/widgets/form_section.dart';
import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart';
import 'package:mostro_mobile/shared/providers/order_repository_provider.dart';
import 'package:mostro_mobile/features/mostro/mostro_instance.dart';
import 'package:mostro_mobile/features/settings/settings_provider.dart';
import 'package:uuid/uuid.dart';
import 'package:mostro_mobile/generated/l10n.dart';
Expand All @@ -40,6 +42,7 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {

int? _minFiatAmount;
int? _maxFiatAmount;
String? _validationError;

List<String> _selectedPaymentMethods = [];
bool _showCustomPaymentMethod = false;
Expand All @@ -63,10 +66,12 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {

// Reset selectedFiatCodeProvider to default from settings for each new order
final settings = ref.read(settingsProvider);
ref.read(selectedFiatCodeProvider.notifier).state = settings.defaultFiatCode;

ref.read(selectedFiatCodeProvider.notifier).state =
settings.defaultFiatCode;

// Pre-populate lightning address from settings if available
if (settings.defaultLightningAddress != null && settings.defaultLightningAddress!.isNotEmpty) {
if (settings.defaultLightningAddress != null &&
settings.defaultLightningAddress!.isNotEmpty) {
_lightningAddressController.text = settings.defaultLightningAddress!;
}
});
Expand All @@ -85,9 +90,99 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
setState(() {
_minFiatAmount = minAmount;
_maxFiatAmount = maxAmount;

// Use comprehensive validation to check all error conditions
_validationError = _validateAllAmounts();
});
}

/// Converts fiat amount to sats using exchange rate
/// Formula: fiatAmount / exchangeRate * 100000000 (sats per BTC)
int _calculateSatsFromFiat(double fiatAmount, double exchangeRate) {
if (exchangeRate <= 0) return 0;
return (fiatAmount / exchangeRate * 100000000).round();
}

/// Validates if sats amount is within mostro instance allowed range
/// Returns error message if validation fails or data is missing
String? _validateSatsRange(double fiatAmount) {
// Ensure fiat code is selected
final selectedFiatCode = ref.read(selectedFiatCodeProvider);
if (selectedFiatCode == null || selectedFiatCode.isEmpty) {
return S.of(context)!.pleaseSelectCurrency;
}

// Get exchange rate - return error if not available
final exchangeRateAsync = ref.read(exchangeRateProvider(selectedFiatCode));
final exchangeRate = exchangeRateAsync.asData?.value;
if (exchangeRate == null) {
// Check if it's loading or error
if (exchangeRateAsync.isLoading) {
return S.of(context)!.exchangeRateNotAvailable;
} else if (exchangeRateAsync.hasError) {
return S.of(context)!.exchangeRateNotAvailable;
}
// Fallback for any other case where rate is null
return S.of(context)!.exchangeRateNotAvailable;
}

// Get mostro instance limits - return error if not available
final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance;
if (mostroInstance == null) {
return S.of(context)!.mostroInstanceNotAvailable;
}

// Calculate sats equivalent
final satsAmount = _calculateSatsFromFiat(fiatAmount, exchangeRate);
final minAllowed = mostroInstance.minOrderAmount;
final maxAllowed = mostroInstance.maxOrderAmount;

// Debug logging
debugPrint(
'Validation: fiat=$fiatAmount, rate=$exchangeRate, sats=$satsAmount, min=$minAllowed, max=$maxAllowed');

// Check if sats amount is outside range
if (satsAmount < minAllowed) {
return S.of(context)!.fiatAmountTooLow(
minAllowed.toString(),
maxAllowed.toString(),
);
} else if (satsAmount > maxAllowed) {
return S.of(context)!.fiatAmountTooHigh(
minAllowed.toString(),
maxAllowed.toString(),
);
}

// Validation passed
return null;
}

/// Comprehensive validation for all fiat amount inputs
/// Returns error message if validation fails, null if all validations pass
String? _validateAllAmounts() {
// Check min/max relationship for range orders
if (_minFiatAmount != null && _maxFiatAmount != null) {
if (_maxFiatAmount! <= _minFiatAmount!) {
return S.of(context)!.maxMustBeGreaterThanMin;
}
}

// Check sats range validation for min amount
if (_minFiatAmount != null) {
final minError = _validateSatsRange(_minFiatAmount!.toDouble());
if (minError != null) return minError;
}

// Check sats range validation for max amount
if (_maxFiatAmount != null) {
final maxError = _validateSatsRange(_maxFiatAmount!.toDouble());
if (maxError != null) return maxError;
}

return null; // All validations passed
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -130,13 +225,18 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
CurrencySection(
orderType: _orderType,
onCurrencySelected: () {
setState(() {});
setState(() {
// Re-validate after currency change since rates/limits differ
_validationError = _validateAllAmounts();
});
},
),
const SizedBox(height: 16),
AmountSection(
orderType: _orderType,
onAmountChanged: _onAmountChanged,
validateSatsRange: _validateSatsRange,
validationError: _validationError,
),
const SizedBox(height: 16),
PaymentMethodsSection(
Expand Down Expand Up @@ -228,7 +328,7 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
child: ActionButtons(
key: const Key('addOrderButtons'),
onCancel: () => context.pop(),
onSubmit: _submitOrder,
onSubmit: _getSubmitCallback(),
currentRequestId: _currentRequestId,
),
),
Expand All @@ -244,6 +344,31 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
);
}

/// Returns submit callback only when form is valid, null otherwise
/// This prevents button loading state when validation errors exist
VoidCallback? _getSubmitCallback() {
// Don't allow submission if validation errors exist
if (_validationError != null) {
return null; // Disables button, prevents loading state
}

// Check other basic conditions that would prevent submission
final selectedFiatCode = ref.read(selectedFiatCodeProvider);
if (selectedFiatCode == null || selectedFiatCode.isEmpty) {
return null;
}

if (_selectedPaymentMethods.isEmpty) {
return null;
}

if (_validationError != null) {
return null;
}

return _submitOrder; // Form is valid - allow submission
}

void _submitOrder() {
if (_formKey.currentState?.validate() ?? false) {
final selectedFiatCode = ref.read(selectedFiatCodeProvider);
Expand Down Expand Up @@ -271,6 +396,32 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {
return;
}

// Enhanced validation: check sats range for both min and max amounts
// This is a critical final validation before submission
if (_validationError != null) {
debugPrint(
'Submission blocked: Validation error present: $_validationError');
// Validation error is already displayed inline, just prevent submission
return;
}

// Additional safety check: ensure we have valid data for submission
final exchangeRateAsync = ref.read(exchangeRateProvider(fiatCode));
final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance;

if (!exchangeRateAsync.hasValue || mostroInstance == null) {
debugPrint(
'Submission blocked: Required data not available - Exchange rate: ${exchangeRateAsync.hasValue}, Mostro instance: ${mostroInstance != null}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(S.of(context)!.exchangeRateNotAvailable),
duration: const Duration(seconds: 3),
backgroundColor: Colors.orange.withValues(alpha: 0.8),
),
);
return;
}

try {
final uuid = const Uuid();
final tempOrderId = uuid.v4();
Expand All @@ -290,32 +441,29 @@ class _AddOrderScreenState extends ConsumerState<AddOrderScreen> {

final satsAmount = int.tryParse(_satsAmountController.text) ?? 0;


List<String> paymentMethods =
List<String>.from(_selectedPaymentMethods);
if (_showCustomPaymentMethod &&
_customPaymentMethodController.text.isNotEmpty) {

// Remove localized "Other" (case-insensitive, trimmed) from the list
final localizedOther = S.of(context)!.other.trim().toLowerCase();
paymentMethods.removeWhere(
(method) => method.trim().toLowerCase() == localizedOther,
);

String sanitizedPaymentMethod = _customPaymentMethodController.text;

final problematicChars = RegExp(r'[,"\\\[\]{}]');
sanitizedPaymentMethod = sanitizedPaymentMethod
.replaceAll(problematicChars, ' ')
.replaceAll(RegExp(r'\s+'), ' ')
.trim();

if (sanitizedPaymentMethod.isNotEmpty) {
paymentMethods.add(sanitizedPaymentMethod);
}
}


final buyerInvoice = _orderType == OrderType.buy &&
_lightningAddressController.text.isNotEmpty
? _lightningAddressController.text
Expand Down
14 changes: 9 additions & 5 deletions lib/features/order/widgets/action_buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import 'package:mostro_mobile/generated/l10n.dart';

class ActionButtons extends StatelessWidget {
final VoidCallback onCancel;
final VoidCallback onSubmit;
final VoidCallback? onSubmit;
final int? currentRequestId;

const ActionButtons({
super.key,
required this.onCancel,
required this.onSubmit,
this.onSubmit,
required this.currentRequestId,
});

Expand Down Expand Up @@ -50,9 +50,13 @@ class ActionButtons extends StatelessWidget {
action: nostr_action.Action.newOrder,
onPressed: onSubmit,
timeout: const Duration(seconds: 5),
showSuccessIndicator: true,
backgroundColor: AppTheme.purpleButton,
foregroundColor: Colors.white,
showSuccessIndicator:
onSubmit != null, // Only show success indicator when enabled
backgroundColor: onSubmit != null
? AppTheme.purpleButton
: AppTheme.backgroundInactive,
foregroundColor:
onSubmit != null ? Colors.white : AppTheme.textInactive,
),
),
),
Expand Down
Loading
Loading