Skip to content
Draft
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
83 changes: 83 additions & 0 deletions lib/packages/service/dfx/dfx_config_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import 'dart:convert';

import 'package:clock/clock.dart';
import 'package:realunit_wallet/packages/config/api_config.dart';
import 'package:realunit_wallet/packages/service/app_store.dart';
import 'package:realunit_wallet/packages/service/dfx/models/config/dto/dfx_config_dto.dart';

/// Fetches the API's authoritative input validation patterns from
/// `GET /v1/config`.
///
/// The app validates name/address fields against the pattern the API serves
/// instead of hardcoding a copy of the `swissPaymentText` regex. The server
/// keeps re-validating authoritatively (`@IsSwissPaymentText()`), so this is
/// purely for instant inline UX — never a gate the backend doesn't also
/// enforce.
///
/// The compiled pattern is cached in memory so form validators can run
/// synchronously ([isSwissPaymentText]); a TTL lets mid-session backend
/// changes surface without an app restart.
class DfxConfigService {
static const _configPath = '/v1/config';

/// How long a fetched config stays cached before the next call refetches.
/// The pattern changes only on a backend deploy, so a generous TTL is fine.
static const _cacheTtl = Duration(minutes: 30);

DfxConfigDto? _cached;
DateTime? _cachedAt;
RegExp? _swissPaymentText;

final AppStore _appStore;

String get _host => _appStore.apiConfig.apiHost;

DfxConfigService(AppStore appStore) : _appStore = appStore;

Future<DfxConfigDto> getConfig() async {
final cached = _cached;
final cachedAt = _cachedAt;
if (cached != null && cachedAt != null && clock.now().difference(cachedAt) < _cacheTtl) {
return cached;
}

final uri = buildUri(_host, _configPath);
final response = await _appStore.httpClient.get(uri);

if (response.statusCode != 200) {
throw Exception('Failed to fetch config: ${response.statusCode}');
}

final config = DfxConfigDto.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
_cached = config;
_cachedAt = clock.now();
_swissPaymentText = config.formats.swissPaymentText.toRegExp();
return config;
}

/// Warms the in-memory cache so [isSwissPaymentText] can validate
/// synchronously. Safe to call eagerly at startup; failures are the
/// caller's concern (the validator degrades gracefully if unloaded).
Future<void> load() => getConfig();

/// Returns `true` if [value] is accepted by the API's `swissPaymentText`
/// pattern. Empty/null is valid (callers chain a non-empty check).
///
/// If the pattern has not loaded yet, returns `true` (does not block) — the
/// server re-validates authoritatively, so the app is never *more*
/// restrictive than the API and never needs a hardcoded fallback copy.
bool isSwissPaymentText(String? value) {
if (value == null || value.isEmpty) return true;
final pattern = _swissPaymentText;
if (pattern == null) return true;
return pattern.hasMatch(value);
}

/// Drops the in-memory cache so the next [getConfig]/[load] refetches.
/// Intended for explicit invalidation points (e.g. network switch).
void invalidateCache() {
_cached = null;
_cachedAt = null;
_swissPaymentText = null;
}
}
61 changes: 61 additions & 0 deletions lib/packages/service/dfx/models/config/dto/dfx_config_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/// DTOs for `GET /v1/config` — the API's authoritative input validation
/// patterns. The app compiles these at runtime instead of hardcoding a copy
/// of the rules (see `DfxConfigService`).
library;

/// A single validation pattern as exposed by the API: a JavaScript `RegExp`
/// source plus its flags.
class DfxValidationFormatDto {
final String pattern;
final String flags;

const DfxValidationFormatDto({required this.pattern, required this.flags});

factory DfxValidationFormatDto.fromJson(Map<String, dynamic> json) {
return DfxValidationFormatDto(
pattern: json['pattern'] as String,
flags: json['flags'] as String,
);
}

/// Compiles the API-provided source/flags into a Dart [RegExp]. Maps the
/// JavaScript flag characters this app cares about onto their Dart
/// `RegExp` equivalents.
RegExp toRegExp() {
return RegExp(
pattern,
unicode: flags.contains('u'),
multiLine: flags.contains('m'),
caseSensitive: !flags.contains('i'),
dotAll: flags.contains('s'),
);
}
}

/// The `formats` map of the config response. Mirrors the API DTO for type
/// safety only — the values themselves are the source of truth.
class DfxFormatsDto {
final DfxValidationFormatDto swissPaymentText;

const DfxFormatsDto({required this.swissPaymentText});

factory DfxFormatsDto.fromJson(Map<String, dynamic> json) {
return DfxFormatsDto(
swissPaymentText: DfxValidationFormatDto.fromJson(
json['swissPaymentText'] as Map<String, dynamic>,
),
);
}
}

class DfxConfigDto {
final DfxFormatsDto formats;

const DfxConfigDto({required this.formats});

factory DfxConfigDto.fromJson(Map<String, dynamic> json) {
return DfxConfigDto(
formats: DfxFormatsDto.fromJson(json['formats'] as Map<String, dynamic>),
);
}
}
30 changes: 0 additions & 30 deletions lib/packages/utils/swiss_payment_text.dart

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_config_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart';
import 'package:realunit_wallet/packages/utils/swiss_payment_text.dart';
import 'package:realunit_wallet/setup/di.dart';
import 'package:realunit_wallet/widgets/buttons/app_filled_button.dart';
import 'package:realunit_wallet/widgets/form/country_field.dart';
import 'package:realunit_wallet/widgets/form/labeled_text_field.dart';
Expand Down Expand Up @@ -54,7 +55,9 @@ class KycRegistrationAddressStep extends StatelessWidget {
textCapitalization: TextCapitalization.words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -67,7 +70,9 @@ class KycRegistrationAddressStep extends StatelessWidget {
keyboardType: TextInputType.streetAddress,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -87,7 +92,9 @@ class KycRegistrationAddressStep extends StatelessWidget {
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -102,7 +109,9 @@ class KycRegistrationAddressStep extends StatelessWidget {
textCapitalization: TextCapitalization.words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_config_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart';
import 'package:realunit_wallet/packages/service/dfx/models/registration/registration_user_type.dart';
import 'package:realunit_wallet/packages/utils/swiss_payment_text.dart';
import 'package:realunit_wallet/screens/kyc/steps/registration/cubits/registration_step/kyc_registration_step_cubit.dart';
import 'package:realunit_wallet/setup/di.dart';
import 'package:realunit_wallet/widgets/buttons/app_filled_button.dart';
import 'package:realunit_wallet/widgets/form/birthday_field.dart';
import 'package:realunit_wallet/widgets/form/country_field.dart';
Expand Down Expand Up @@ -72,7 +73,9 @@ class KycRegistrationPersonalStep extends StatelessWidget {
textCapitalization: TextCapitalization.words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -86,7 +89,9 @@ class KycRegistrationPersonalStep extends StatelessWidget {
textCapitalization: TextCapitalization.words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_config_service.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/service/dfx/models/country/country.dart';
import 'package:realunit_wallet/packages/utils/swiss_payment_text.dart';
import 'package:realunit_wallet/packages/utils/xfile_extension.dart';
import 'package:realunit_wallet/screens/settings_user_data/subpages/edit_address/cubit/settings_edit_address_cubit.dart';
import 'package:realunit_wallet/screens/settings_user_data/subpages/others/settings_edit_failure_page.dart';
Expand Down Expand Up @@ -112,7 +112,11 @@ class _SettingsEditAddressViewState extends State<SettingsEditAddressView> {
textCapitalization: .words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(
value,
)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -125,7 +129,11 @@ class _SettingsEditAddressViewState extends State<SettingsEditAddressView> {
keyboardType: .streetAddress,
validator: (value) {
if (value == null || value.isEmpty) return null;
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(
value,
)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -145,7 +153,11 @@ class _SettingsEditAddressViewState extends State<SettingsEditAddressView> {
keyboardType: .number,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(
value,
)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -160,7 +172,11 @@ class _SettingsEditAddressViewState extends State<SettingsEditAddressView> {
textCapitalization: .words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(
value,
)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:realunit_wallet/generated/i18n.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_config_service.dart';
import 'package:realunit_wallet/packages/service/dfx/dfx_kyc_service.dart';
import 'package:realunit_wallet/packages/utils/swiss_payment_text.dart';
import 'package:realunit_wallet/packages/utils/xfile_extension.dart';
import 'package:realunit_wallet/screens/settings_user_data/subpages/edit_name/cubit/settings_edit_name_cubit.dart';
import 'package:realunit_wallet/screens/settings_user_data/subpages/others/settings_edit_failure_page.dart';
Expand Down Expand Up @@ -99,7 +99,9 @@ class _SettingsEditNameViewState extends State<SettingsEditNameView> {
textCapitalization: .words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand All @@ -110,7 +112,9 @@ class _SettingsEditNameViewState extends State<SettingsEditNameView> {
textCapitalization: .words,
validator: (value) {
if (value == null || value.isEmpty) return '';
if (!isSwissPaymentText(value)) return S.of(context).swissPaymentTextInvalid;
if (!getIt<DfxConfigService>().isSwissPaymentText(value)) {
return S.of(context).swissPaymentTextInvalid;
}
return null;
},
),
Expand Down
Loading