diff --git a/assets/translations/en.json b/assets/translations/en.json index 23fb34409a..35b8208308 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -653,6 +653,10 @@ "statistics": "Statistics", "ibcTransferFieldTitle": "IBC Transfer", "ibcTransferFieldSubtitle": "Send to another Cosmos chain", + "ibcChannel": "IBC Channel", + "ibcChannelHint": "channel-141", + "ibcChannelRequired": "IBC channel is required", + "ibcChannelInvalidFormat": "Invalid format. Use: channel-", "successPageHeadline": "Withdrawal Successful", "successPageBodySmall": "Transaction Hash:", "withdrawErrorCardTileTitle": "Technical Details", diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index 232b07dcc0..f47235be96 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -346,24 +346,30 @@ class WithdrawFormBloc extends Bloc { WithdrawFormIbcChannelChanged event, Emitter emit, ) { - if (event.channel.isEmpty) { - emit( - state.copyWith( - ibcChannel: () => event.channel, - ibcChannelError: () => TextError(error: 'Channel ID is required'), - ), - ); - return; - } + final ibcChannelError = _validateIbcChannel(event.channel); emit( state.copyWith( ibcChannel: () => event.channel, - ibcChannelError: () => null, + ibcChannelError: () => ibcChannelError, ), ); } + /// Validate format: channel-{number} + TextError? _validateIbcChannel(String channel) { + if (channel.isEmpty) { + return TextError(error: 'Channel ID is required'); + } + + final channelRegex = RegExp(r'^channel-\d+$'); + if (!channelRegex.hasMatch(channel)) { + return TextError(error: 'Invalid format. Use: channel-'); + } + + return null; + } + Future _onPreviewSubmitted( WithdrawFormPreviewSubmitted event, Emitter emit, diff --git a/lib/bloc/withdraw_form/withdraw_form_state.dart b/lib/bloc/withdraw_form/withdraw_form_state.dart index 0ae7e408a7..5dc4e3e38a 100644 --- a/lib/bloc/withdraw_form/withdraw_form_state.dart +++ b/lib/bloc/withdraw_form/withdraw_form_state.dart @@ -43,8 +43,7 @@ class WithdrawFormState extends Equatable { bool get hasPreviewError => previewError != null; bool get hasTransactionError => transactionError != null; - bool get hasAddressError => - recipientAddressError != null; + bool get hasAddressError => recipientAddressError != null; bool get hasValidationErrors => hasAddressError || amountError != null || @@ -186,6 +185,8 @@ class WithdrawFormState extends Equatable { : null, memo: memo, ibcTransfer: isIbcTransfer ? true : null, + ibcSourceChannel: + ibcChannel?.isNotEmpty == true ? ibcChannel!.trim() : null, isMax: isMaxAmount, ); } diff --git a/lib/generated/codegen_loader.g.dart b/lib/generated/codegen_loader.g.dart index 71f8bd61d3..d8a86fb578 100644 --- a/lib/generated/codegen_loader.g.dart +++ b/lib/generated/codegen_loader.g.dart @@ -650,6 +650,10 @@ abstract class LocaleKeys { static const statistics = 'statistics'; static const ibcTransferFieldTitle = 'ibcTransferFieldTitle'; static const ibcTransferFieldSubtitle = 'ibcTransferFieldSubtitle'; + static const ibcChannel = 'ibcChannel'; + static const ibcChannelHint = 'ibcChannelHint'; + static const ibcChannelRequired = 'ibcChannelRequired'; + static const ibcChannelInvalidFormat = 'ibcChannelInvalidFormat'; static const successPageHeadline = 'successPageHeadline'; static const successPageBodySmall = 'successPageBodySmall'; static const withdrawErrorCardTileTitle = 'withdrawErrorCardTileTitle'; diff --git a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart index d4ec4670df..48c92249b3 100644 --- a/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart +++ b/lib/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart @@ -112,7 +112,8 @@ class FeeSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(LocaleKeys.networkFee.tr(), style: Theme.of(context).textTheme.titleMedium), + Text(LocaleKeys.networkFee.tr(), + style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), const CustomFeeToggle(), if (state.isCustomFee) ...[ @@ -578,17 +579,50 @@ class IbcTransferField extends StatelessWidget { } } -class IbcChannelField extends StatelessWidget { +class IbcChannelField extends StatefulWidget { const IbcChannelField({super.key}); + @override + State createState() => _IbcChannelFieldState(); +} + +class _IbcChannelFieldState extends State { + late final TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listenWhen: (previous, current) => + previous.ibcChannel != current.ibcChannel && + current.ibcChannel != _controller.text, + listener: (context, state) { + // Only update controller if the bloc state differs from current text + // This prevents the cursor from jumping when user is typing + if (state.ibcChannel != _controller.text) { + _controller.text = state.ibcChannel ?? ''; + } + }, + buildWhen: (previous, current) => + previous.ibcChannelError != current.ibcChannelError, builder: (context, state) { return UiTextFormField( key: const Key('withdraw-ibc-channel-input'), - labelText: 'IBC Channel', - hintText: 'Enter IBC channel ID', + controller: _controller, + labelText: LocaleKeys.ibcChannel.tr(), + hintText: LocaleKeys.ibcChannelHint.tr(), + errorText: state.ibcChannelError?.message, onChanged: (value) { context .read() @@ -596,8 +630,15 @@ class IbcChannelField extends StatelessWidget { }, validator: (value) { if (value?.isEmpty ?? true) { - return 'Please enter IBC channel'; + return LocaleKeys.ibcChannelRequired.tr(); + } + + // Validate format: channel- + final channelRegex = RegExp(r'^channel-\d+$'); + if (!channelRegex.hasMatch(value!)) { + return LocaleKeys.ibcChannelInvalidFormat.tr(); } + return null; }, ); diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index ece0a9d584..376aaf8e40 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -12,6 +12,7 @@ import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; +import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fields.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; class WithdrawForm extends StatefulWidget { @@ -304,6 +305,14 @@ class WithdrawFormFillSection extends StatelessWidget { : () => state.recipientAddressError?.message, ), const SizedBox(height: 16), + if (state.asset.protocol is TendermintProtocol) ...[ + const IbcTransferField(), + if (state.isIbcTransfer) ...[ + const SizedBox(height: 16), + const IbcChannelField(), + ], + const SizedBox(height: 16), + ], WithdrawAmountField( asset: state.asset, amount: state.amount, diff --git a/packages/komodo_ui_kit/pubspec.lock b/packages/komodo_ui_kit/pubspec.lock index e8ac3a0b35..80591e97ba 100644 --- a/packages/komodo_ui_kit/pubspec.lock +++ b/packages/komodo_ui_kit/pubspec.lock @@ -95,7 +95,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -104,7 +104,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -113,7 +113,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" diff --git a/pubspec.lock b/pubspec.lock index a60797aa1b..376b1dad62 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -648,7 +648,7 @@ packages: description: path: "packages/komodo_cex_market_data" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.0.1" @@ -657,7 +657,7 @@ packages: description: path: "packages/komodo_coins" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -666,7 +666,7 @@ packages: description: path: "packages/komodo_defi_framework" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0" @@ -675,7 +675,7 @@ packages: description: path: "packages/komodo_defi_local_auth" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -684,7 +684,7 @@ packages: description: path: "packages/komodo_defi_rpc_methods" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -693,7 +693,7 @@ packages: description: path: "packages/komodo_defi_sdk" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -702,7 +702,7 @@ packages: description: path: "packages/komodo_defi_types" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -718,7 +718,7 @@ packages: description: path: "packages/komodo_ui" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0" @@ -734,7 +734,7 @@ packages: description: path: "packages/komodo_wallet_build_transformer" ref: dev - resolved-ref: "41b554d08ed3f42f9f784a488cedf9ab4b3b3313" + resolved-ref: "4ecbdaba11f572aa3c46603622ad42d7ee995b1a" url: "https://github.com/KomodoPlatform/komodo-defi-sdk-flutter.git" source: git version: "0.2.0+0"