diff --git a/deploy/package.json b/deploy/package.json index b37c1660be..154fe0d17d 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -11,5 +11,4 @@ "dependencies": { "permaweb-deploy": "1.1.6" } - } diff --git a/lib/arns/domain/arns_repository.dart b/lib/arns/domain/arns_repository.dart index 298ce59e72..61ef0d59df 100644 --- a/lib/arns/domain/arns_repository.dart +++ b/lib/arns/domain/arns_repository.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:ardrive/arns/data/arns_dao.dart'; +import 'package:ardrive/arns/domain/exceptions.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/core/arfs/repository/file_repository.dart'; import 'package:ardrive/entities/profile_types.dart'; @@ -21,6 +22,7 @@ abstract class ARNSRepository { required String processId, bool uploadNewRevision = true, }); + Future createUndername({required ARNSUndername undername}); Future> getAntRecordsForWallet(String address, {bool update = false}); Future> getARNSUndernames(sdk.ANTRecord record, @@ -34,6 +36,7 @@ abstract class ARNSRepository { Future waitForARNSRecordsToUpdate(); Future getUndernameByDomainAndName( String domain, String name); + Future> getARNSNameModelsForWallet(String address); Future getPrimaryName(String address, {bool update = false, bool getLogo = true}); @@ -185,6 +188,53 @@ class _ARNSRepository implements ARNSRepository { } } + @override + Future createUndername({ + required ARNSUndername undername, + }) async { + // verify if the undername already exists + final undernames = _cachedUndernames[undername.domain]!; + + if (undernames.containsKey(undername.name)) { + throw UndernameAlreadyExistsException(); + } + + if (_auth.currentUser.profileType == ProfileType.arConnect) { + logger.d('Setting undername with ArConnect'); + + final id = await _sdk.setUndernameWithArConnect( + txId: undername.record.transactionId, + domain: undername.domain, + undername: undername.name, + ); + + logger.d('Undername set with ArConnect: $id'); + } else { + await _sdk.setUndername( + jwtString: _auth.getJWTAsString(), + domain: undername.domain, + txId: undername.record.transactionId, + undername: undername.name, + ); + } + + _cachedUndernames[undername.domain]![undername.name] = undername; + + await _arnsDao.saveARNSRecord( + domain: undername.domain, + transactionId: undername.record.transactionId, + isActive: true, + undername: undername.name, + ttl: undername.record.ttlSeconds, + fileId: '', // we don't have a file id for the undername + ); + + await updateARNSRecordsActiveStatus( + domain: undername.domain, + name: undername.name, + ); + } + Completer>? _getARNSUndernamesCompleter; @override @@ -397,6 +447,12 @@ class _ARNSRepository implements ARNSRepository { return undername; } + @override + Future> getARNSNameModelsForWallet( + String address) async { + return _sdk.getArNSNames(address); + } + @override Future getPrimaryName(String address, {bool update = false, bool getLogo = true}) async { diff --git a/lib/arns/domain/exceptions.dart b/lib/arns/domain/exceptions.dart new file mode 100644 index 0000000000..bfb576f4b1 --- /dev/null +++ b/lib/arns/domain/exceptions.dart @@ -0,0 +1 @@ +class UndernameAlreadyExistsException implements Exception {} diff --git a/lib/arns/presentation/assign_name_bloc/assign_name_bloc.dart b/lib/arns/presentation/assign_name_bloc/assign_name_bloc.dart index ecc369cf3d..9d51af0064 100644 --- a/lib/arns/presentation/assign_name_bloc/assign_name_bloc.dart +++ b/lib/arns/presentation/assign_name_bloc/assign_name_bloc.dart @@ -14,7 +14,8 @@ class AssignNameBloc extends Bloc { final ARNSRepository _arnsRepository; final ArDriveAuth _auth; ARNSUndername? _selectedUndername; - ANTRecord? _selectedANTRecord; + // TODO: remove this later + ArNSNameModel? _selectedNameModel; AssignNameBloc({ required ArDriveAuth auth, @@ -23,52 +24,90 @@ class AssignNameBloc extends Bloc { }) : _auth = auth, _arnsRepository = arnsRepository, super(AssignNameInitial()) { - on((event, emit) async { - try { - emit(LoadingNames()); - - final walletAddress = await _auth.getWalletAddress(); - - final names = - await _arnsRepository.getAntRecordsForWallet(walletAddress!); - - if (names.isEmpty) { - emit(AssignNameEmptyState()); - } else { - emit(NamesLoaded(names: names)); + on( + (event, emit) async { + try { + logger.d('Loading names'); + emit(LoadingNames()); + + final walletAddress = await _auth.getWalletAddress(); + + final names = await _arnsRepository + .getAntRecordsForWallet(walletAddress!, update: false); + + final nameModels = + await _arnsRepository.getARNSNameModelsForWallet(walletAddress); + + if (names.isEmpty) { + emit(AssignNameEmptyState()); + } else { + logger.d('Names loaded'); + emit(NamesLoaded(nameModels: nameModels)); + } + } catch (e) { + logger.e('Failed to load ArNS names', e); + emit(LoadingNamesFailed()); } - } catch (e) { - logger.e('Failed to load ArNS names', e); - emit(LoadingNamesFailed()); - } - }); + }, + ); on((event, emit) async { - _selectedANTRecord = event.name; - if (state is NamesLoaded) { emit( (state as NamesLoaded).copyWith( - selectedName: event.name, + selectedName: event.nameModel, ), ); } - if (state is UndernamesLoaded) { + + if (state is UndernamesLoaded && + _selectedNameModel?.name == event.nameModel.name) { emit( NamesLoaded( - names: (state as UndernamesLoaded).names, - selectedName: _selectedANTRecord), + nameModels: (state as UndernamesLoaded).nameModels, + selectedName: _selectedNameModel, + ), ); } + + _selectedNameModel = event.nameModel; }); on( (event, emit) async { - final names = (state as NamesLoaded).names; + if (state is UndernamesLoaded) { + final undernames = await _arnsRepository.getARNSUndernames( + ANTRecord( + domain: _selectedNameModel!.name, + processId: _selectedNameModel!.processId, + ), + ); + + if (undernames.length > 1) { + undernames.removeWhere((element) => element.name == '@'); + } + + emit( + UndernamesLoaded( + nameModels: (state as UndernamesLoaded).nameModels, + selectedName: _selectedNameModel!, + undernames: undernames, + selectedUndername: null, + ), + ); + + return; + } + + final names = (state as NamesLoaded).nameModels; emit(LoadingUndernames()); - final undernames = - await _arnsRepository.getARNSUndernames(_selectedANTRecord!); + final undernames = await _arnsRepository.getARNSUndernames( + ANTRecord( + domain: _selectedNameModel!.name, + processId: _selectedNameModel!.processId, + ), + ); if (undernames.length > 1) { undernames.removeWhere((element) => element.name == '@'); @@ -76,8 +115,8 @@ class AssignNameBloc extends Bloc { emit( UndernamesLoaded( - selectedName: _selectedANTRecord!, - names: names, + nameModels: names, + selectedName: _selectedNameModel!, undernames: undernames, selectedUndername: null, ), @@ -111,7 +150,7 @@ class AssignNameBloc extends Bloc { name: '@', record: ARNSRecord( transactionId: fileDataTableItem.dataTxId, ttlSeconds: 3600), - domain: _selectedANTRecord!.domain, + domain: _selectedNameModel!.name, ); } else { undername = ARNSUndername( @@ -120,7 +159,7 @@ class AssignNameBloc extends Bloc { transactionId: fileDataTableItem.dataTxId, ttlSeconds: 3600, ), - domain: _selectedANTRecord!.domain, + domain: _selectedNameModel!.name, ); } @@ -128,11 +167,11 @@ class AssignNameBloc extends Bloc { undername: undername, fileId: fileDataTableItem.fileId, driveId: fileDataTableItem.driveId, - processId: _selectedANTRecord!.processId, + processId: _selectedNameModel!.processId, ); final (address, arAddress) = getAddressesFromArns( - domain: _selectedANTRecord!.domain, + domain: _selectedNameModel!.name, undername: _selectedUndername?.name, ); @@ -146,9 +185,21 @@ class AssignNameBloc extends Bloc { } }); + on((event, emit) async { + final (address, arAddress) = getAddressesFromArns( + domain: event.undername.domain, + undername: event.undername.name, + ); + + emit(NameAssignedWithSuccess( + address: address, + arAddress: arAddress, + )); + }); + on((event, emit) async { emit(SelectionConfirmed( - selectedName: _selectedANTRecord!, + selectedName: _selectedNameModel!, selectedUndername: _selectedUndername, )); }); diff --git a/lib/arns/presentation/assign_name_bloc/assign_name_event.dart b/lib/arns/presentation/assign_name_bloc/assign_name_event.dart index 369f23dc63..e065f9a894 100644 --- a/lib/arns/presentation/assign_name_bloc/assign_name_event.dart +++ b/lib/arns/presentation/assign_name_bloc/assign_name_event.dart @@ -18,12 +18,11 @@ final class LoadNames extends AssignNameEvent { final class CloseAssignName extends AssignNameEvent {} final class SelectName extends AssignNameEvent { - final ANTRecord name; - - const SelectName(this.name); + final ArNSNameModel nameModel; + const SelectName(this.nameModel); @override - List get props => [name]; + List get props => [nameModel]; } final class LoadUndernames extends AssignNameEvent { @@ -47,3 +46,13 @@ final class SelectUndername extends AssignNameEvent { final class ConfirmSelectionAndUpload extends AssignNameEvent {} final class ConfirmSelection extends AssignNameEvent {} + +final class ShowSuccessModal extends AssignNameEvent { + final ARNSUndername undername; + + const ShowSuccessModal({required this.undername}); + + @override + List get props => [undername]; +} +// diff --git a/lib/arns/presentation/assign_name_bloc/assign_name_state.dart b/lib/arns/presentation/assign_name_bloc/assign_name_state.dart index 10b609dcf7..e2e2552137 100644 --- a/lib/arns/presentation/assign_name_bloc/assign_name_state.dart +++ b/lib/arns/presentation/assign_name_bloc/assign_name_state.dart @@ -12,20 +12,20 @@ final class AssignNameInitial extends AssignNameState {} final class LoadingNames extends AssignNameState {} final class NamesLoaded extends AssignNameState { - final List names; - final ANTRecord? selectedName; + final List nameModels; + final ArNSNameModel? selectedName; - const NamesLoaded({required this.names, this.selectedName}); + const NamesLoaded({required this.nameModels, this.selectedName}); @override - List get props => [names, selectedName]; + List get props => [nameModels, selectedName]; NamesLoaded copyWith({ - List? names, - ANTRecord? selectedName, + List? nameModels, + ArNSNameModel? selectedName, }) { return NamesLoaded( - names: names ?? this.names, + nameModels: nameModels ?? this.nameModels, selectedName: selectedName ?? this.selectedName, ); } @@ -34,13 +34,13 @@ final class NamesLoaded extends AssignNameState { final class AssignNameEmptyState extends AssignNameState {} final class UndernamesLoaded extends AssignNameState { - final List names; - final ANTRecord selectedName; + final List nameModels; final List undernames; final ARNSUndername? selectedUndername; + final ArNSNameModel? selectedName; const UndernamesLoaded({ - required this.names, + required this.nameModels, required this.undernames, required this.selectedUndername, required this.selectedName, @@ -48,20 +48,20 @@ final class UndernamesLoaded extends AssignNameState { @override List get props => [ - names, + nameModels, selectedName, undernames, selectedUndername, ]; UndernamesLoaded copyWith({ - List? names, - ANTRecord? selectedName, List? undernames, ARNSUndername? selectedUndername, + List? nameModels, + ArNSNameModel? selectedName, }) { return UndernamesLoaded( - names: names ?? this.names, + nameModels: nameModels ?? this.nameModels, selectedName: selectedName ?? this.selectedName, undernames: undernames ?? this.undernames, selectedUndername: selectedUndername ?? this.selectedUndername, @@ -80,7 +80,7 @@ final class NameAssignedWithSuccess extends AssignNameState { } final class SelectionConfirmed extends AssignNameState { - final ANTRecord selectedName; + final ArNSNameModel selectedName; final ARNSUndername? selectedUndername; const SelectionConfirmed({ diff --git a/lib/arns/presentation/assign_name_modal.dart b/lib/arns/presentation/assign_name_modal.dart index 26d04c4aa6..6fe7ec105c 100644 --- a/lib/arns/presentation/assign_name_modal.dart +++ b/lib/arns/presentation/assign_name_modal.dart @@ -2,10 +2,12 @@ import 'package:ardrive/arns/domain/arns_repository.dart'; import 'package:ardrive/arns/presentation/assign_name_bloc/assign_name_bloc.dart'; +import 'package:ardrive/arns/presentation/create_undername.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/misc/resources.dart'; +import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/drive_detail/models/data_table_item.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/utils/logger.dart'; @@ -68,6 +70,8 @@ class AssignArNSNameModal extends StatelessWidget { @override Widget build(BuildContext context) { + logger.d('AssignArNSNameModal build'); + return BlocProvider( create: (context) => AssignNameBloc( auth: context.read(), @@ -117,6 +121,12 @@ class _AssignArNSNameModal extends StatefulWidget { } class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { + @override + dispose() { + super.dispose(); + logger.d('AssignArNSNameModal dispose'); + } + @override Widget build(BuildContext context) { final typography = ArDriveTypographyNew.of(context); @@ -169,7 +179,7 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { ? null : kLargeDialogWidth, content: Builder( - builder: (context) { + builder: (_) { if (state is LoadingNames || state is AssignNameInitial) { return const SizedBox( height: 275, @@ -188,9 +198,9 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { const SizedBox( height: 16, ), - _NameSelectorDropdown( + _NameSelectorDropdown( label: 'ArNS name', - names: state.names, + names: state.nameModels, hintText: 'Choose ArNS name', selectedName: state.selectedName, onSelected: (name) { @@ -224,6 +234,10 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { ], ); } else if (state is UndernamesLoaded) { + final undernamesPurchased = state.undernames + .where((element) => element.name != '@') + .length; + return ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.5), @@ -238,10 +252,10 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { const SizedBox( height: 16, ), - _NameSelectorDropdown( + _NameSelectorDropdown( selectedName: state.selectedName, label: 'ArNS name', - names: state.names, + names: state.nameModels, hintText: 'Choose ArNS name', onSelected: (name) { context.read().add(SelectName(name)); @@ -253,16 +267,86 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { const SizedBox( height: 40, ), - _NameSelectorDropdown( - label: 'under_name (optional)', - names: state.undernames, - selectedName: state.selectedUndername, - hintText: 'Select undername', - onSelected: (name) { - context - .read() - .add(SelectUndername(undername: name)); - }, + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _NameSelectorDropdown( + label: 'under_name (optional)', + names: state.undernames, + selectedName: state.selectedUndername, + hintText: 'Select undername', + onSelected: (name) { + logger.d('Selecting undername'); + context + .read() + .add(SelectUndername(undername: name)); + }, + records: undernamesPurchased, + undernameLimit: state.selectedName?.undernameLimit, + key: ValueKey(state.selectedName?.name), + onCreateNewUndername: () { + if (state.undernames.length == + state.selectedName?.undernameLimit) { + return; + } + + showArDriveDialog( + context, + content: BlocProvider.value( + value: context.read(), + child: CreateUndernameModal( + nameModel: state.selectedName!, + driveId: widget.file!.driveId, + fileId: widget.file!.id, + transactionId: widget.file!.dataTxId, + ), + ), + ); + }, + ), + const SizedBox(width: 8), + Padding( + padding: const EdgeInsets.only(top: 30.0), + child: ArDriveIconButton( + tooltip: undernamesPurchased == + state.selectedName?.undernameLimit + ? 'You can\'t create more undernames $undernamesPurchased of ${state.selectedName?.undernameLimit} in use' + : 'Add new undername $undernamesPurchased of ${state.selectedName?.undernameLimit} in use', + icon: ArDriveIcons.addArnsName( + size: 30, + color: undernamesPurchased == + state.selectedName?.undernameLimit + ? ArDriveTheme.of(context) + .themeData + .colorTokens + .buttonDisabled + : ArDriveTheme.of(context) + .themeData + .colorTokens + .textHigh, + ), + onPressed: () { + if (undernamesPurchased == + state.selectedName?.undernameLimit) { + return; + } + + showArDriveDialog( + context, + content: BlocProvider.value( + value: context.read(), + child: CreateUndernameModal( + nameModel: state.selectedName!, + driveId: widget.file!.driveId, + fileId: widget.file!.id, + transactionId: widget.file!.dataTxId, + ), + ), + ); + }, + ), + ) + ], ), ], ), @@ -326,7 +410,7 @@ class _AssignArNSNameModalState extends State<_AssignArNSNameModal> { ModalAction( action: () { Navigator.of(context).pop(); - }, + }, title: 'Cancel', ), ModalAction( @@ -438,14 +522,20 @@ class _NameSelectorDropdown extends StatefulWidget { this.selectedName, required this.label, required this.hintText, + this.records, + this.undernameLimit, + this.onCreateNewUndername, super.key, }); final List names; final Function(T) onSelected; + final Function()? onCreateNewUndername; final T? selectedName; final String label; final String hintText; + final int? records; + final int? undernameLimit; @override State<_NameSelectorDropdown> createState() => @@ -460,6 +550,7 @@ class __NameSelectorDropdownState extends State<_NameSelectorDropdown> { initState() { super.initState(); selectedName = widget.selectedName; + logger.d('Selected name: ${widget.selectedName}'); } @override @@ -477,10 +568,18 @@ class __NameSelectorDropdownState extends State<_NameSelectorDropdown> { maxHeight = 48 * widget.names.length.toDouble(); } + if (widget.onCreateNewUndername != null) { + maxHeight = maxHeight + 48; + } + if (maxWidth >= MediaQuery.of(context).size.width) { maxWidth = MediaQuery.of(context).size.width - 32; } + if (widget.onCreateNewUndername != null) { + maxWidth = maxWidth - 48; + } + return ArDriveDropdown( hasBorder: false, hasDivider: false, @@ -489,7 +588,6 @@ class __NameSelectorDropdownState extends State<_NameSelectorDropdown> { target: Alignment.bottomRight, offset: Offset(0, 10), ), - showScrollbars: true, maxHeight: maxHeight, items: _buildList(widget.names, maxWidth), child: Column( @@ -547,8 +645,8 @@ class __NameSelectorDropdownState extends State<_NameSelectorDropdown> { String _getName(T item) { String name; - if (item is ANTRecord) { - name = item.domain; + if (item is ArNSNameModel) { + name = item.name; } else if (item is ARNSUndername) { name = item.name; } else { @@ -591,6 +689,35 @@ class __NameSelectorDropdownState extends State<_NameSelectorDropdown> { ), ); } + + if (widget.onCreateNewUndername != null) { + // add create new undername button + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + list.add( + ArDriveDropdownItem( + onClick: () { + widget.onCreateNewUndername?.call(); + }, + content: Container( + alignment: Alignment.centerLeft, + width: maxWidth, + color: colorTokens.containerL3, + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'Create new undername. ${widget.records} of ${widget.undernameLimit} in use', + style: ArDriveTypographyNew.of(context).paragraphLarge(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ); + } + return list; } } diff --git a/lib/arns/presentation/create_undername.dart b/lib/arns/presentation/create_undername.dart new file mode 100644 index 0000000000..0ada774253 --- /dev/null +++ b/lib/arns/presentation/create_undername.dart @@ -0,0 +1,160 @@ +import 'package:ardrive/arns/domain/arns_repository.dart'; +import 'package:ardrive/arns/domain/exceptions.dart'; +import 'package:ardrive/arns/presentation/assign_name_bloc/assign_name_bloc.dart'; +import 'package:ardrive/arns/presentation/create_undername/create_undername_bloc.dart'; +import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CreateUndernameModal extends StatelessWidget { + const CreateUndernameModal({ + super.key, + required this.nameModel, + required this.driveId, + required this.fileId, + required this.transactionId, + }); + + final ArNSNameModel nameModel; + final String driveId; + final String fileId; + final String transactionId; + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CreateUndernameBloc( + context.read(), + nameModel, + driveId, + fileId, + transactionId, + ), + child: const CreateUndernameView(), + ); + } +} + +class CreateUndernameView extends StatefulWidget { + const CreateUndernameView({super.key}); + + @override + State createState() => _CreateUndernameViewState(); +} + +class _CreateUndernameViewState extends State { + final controller = TextEditingController(); + bool isLoading = false; + + @override + void initState() { + super.initState(); + controller.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is CreateUndernameSuccess) { + setState(() { + isLoading = false; + }); + + /// Refresh the AssignNameBloc to update the UI with the new undername in the dropdown list + context.read().add(const LoadUndernames()); + Navigator.pop(context); + } + }, + builder: (context, state) { + Widget content; + List actions = []; + + if (state is CreateUndernameFailure) { + actions.add(ModalAction( + action: () { + Navigator.pop(context); + }, + title: 'OK', + )); + } else { + actions.add(ModalAction( + action: () { + Navigator.pop(context); + }, + title: 'Cancel', + )); + actions.add(ModalAction( + isEnable: controller.text.isNotEmpty && !isLoading, + action: () { + setState(() { + isLoading = true; + }); + context + .read() + .add(CreateNewUndername(controller.text)); + }, + title: 'Create', + )); + } + + final typography = ArDriveTypographyNew.of(context); + + if (state is CreateUndernameSuccess) { + content = Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text('Undername created successfully', + style: typography.paragraphNormal()), + ); + } else if (state is CreateUndernameFailure) { + if (state.exception is UndernameAlreadyExistsException) { + content = Text( + 'Undername already exists. Please choose a different name.', + style: typography.paragraphNormal(), + ); + } else { + content = Text( + 'An error occurred while creating the undername. Please try again.', + style: typography.paragraphNormal(), + ); + } + } else if (state is CreateUndernameLoading) { + content = Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text('Creating undername...', + style: typography.paragraphNormal()), + ); + } else { + content = CreateUndernameForm(onChanged: (text) { + controller.text = text; + }); + } + + return ArDriveStandardModalNew( + width: kMediumDialogWidth, + title: 'Create Undername', + content: content, + actions: actions, + ); + }, + ); + } +} + +class CreateUndernameForm extends StatelessWidget { + const CreateUndernameForm({super.key, required this.onChanged}); + + final Function(String) onChanged; + + @override + Widget build(BuildContext context) { + return ArDriveTextFieldNew( + label: 'Undername', + hintText: 'Enter your undername', + onChanged: onChanged, + ); + } +} diff --git a/lib/arns/presentation/create_undername/create_undername_bloc.dart b/lib/arns/presentation/create_undername/create_undername_bloc.dart new file mode 100644 index 0000000000..cbadc04d24 --- /dev/null +++ b/lib/arns/presentation/create_undername/create_undername_bloc.dart @@ -0,0 +1,53 @@ +import 'package:ardrive/arns/domain/arns_repository.dart'; +import 'package:ardrive/arns/domain/exceptions.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'create_undername_event.dart'; +part 'create_undername_state.dart'; + +class CreateUndernameBloc + extends Bloc { + final ARNSRepository _arnsRepository; + final ArNSNameModel _nameModel; + final String driveId; + final String fileId; + final String transactionId; + CreateUndernameBloc( + this._arnsRepository, + this._nameModel, + this.driveId, + this.fileId, + this.transactionId, + ) : super(CreateUndernameInitial()) { + on((event, emit) async { + if (event is CreateNewUndername) { + emit(CreateUndernameLoading()); + + final ARNSUndername undername = ARNSUndername( + name: event.name, + record: ARNSRecord( + transactionId: transactionId, + ttlSeconds: 3600, + ), + domain: _nameModel.name, + ); + + try { + logger.d('Creating undername...'); + await _arnsRepository.createUndername(undername: undername); + logger.d('Undername created successfully'); + emit(CreateUndernameSuccess(nameModel: _nameModel)); + } on UndernameAlreadyExistsException { + emit(CreateUndernameFailure( + exception: UndernameAlreadyExistsException())); + } catch (e, stacktrace) { + logger.e('Error creating undername.', e, stacktrace); + emit(CreateUndernameFailure(exception: e)); + } + } + }); + } +} diff --git a/lib/arns/presentation/create_undername/create_undername_event.dart b/lib/arns/presentation/create_undername/create_undername_event.dart new file mode 100644 index 0000000000..f47b154ddd --- /dev/null +++ b/lib/arns/presentation/create_undername/create_undername_event.dart @@ -0,0 +1,18 @@ +part of 'create_undername_bloc.dart'; + +sealed class CreateUndernameEvent extends Equatable { + const CreateUndernameEvent(); + + @override + List get props => []; +} + +final class CreateNewUndername extends CreateUndernameEvent { + final String name; + + const CreateNewUndername(this.name); + + @override + List get props => [name]; +} +// diff --git a/lib/arns/presentation/create_undername/create_undername_state.dart b/lib/arns/presentation/create_undername/create_undername_state.dart new file mode 100644 index 0000000000..5915198d02 --- /dev/null +++ b/lib/arns/presentation/create_undername/create_undername_state.dart @@ -0,0 +1,30 @@ +part of 'create_undername_bloc.dart'; + +sealed class CreateUndernameState extends Equatable { + const CreateUndernameState(); + + @override + List get props => []; +} + +final class CreateUndernameInitial extends CreateUndernameState {} + +final class CreateUndernameLoading extends CreateUndernameState {} + +final class CreateUndernameSuccess extends CreateUndernameState { + final ArNSNameModel nameModel; + + const CreateUndernameSuccess({required this.nameModel}); + + @override + List get props => [nameModel]; +} + +final class CreateUndernameFailure extends CreateUndernameState { + final Object exception; + + const CreateUndernameFailure({required this.exception}); + + @override + List get props => [exception]; +} diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index e4c5bb2b8b..bd1926ae1e 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -325,10 +325,11 @@ class _CreateManifestFormState extends State { customLoadingText: 'Fetching ArNS names...', customNameSelectionTitle: 'Assign ArNS Name to New Manifest', onSelectionConfirmed: (selection) { - context.read().selectArns( - selection.selectedName, - selection.selectedUndername, - ); + // TODO: RE-ENABLE THIS + // context.read().selectArns( + // selection.selectedName, + // selection.selectedUndername, + // ); }, ); } diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 46300290d2..d6a3bb7a2e 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -336,10 +336,12 @@ class _UploadFormState extends State { driveDetailCubit: widget.driveDetailCubit, justSelectName: true, onSelectionConfirmed: (name) { - context.read().selectUndernameWithLicense( - antRecord: name.selectedName, - undername: name.selectedUndername, - ); + // TODO: RE-ENABLE THIS + + // context.read().selectUndernameWithLicense( + // antRecord: name.selectedName, + // undername: name.selectedUndername, + // ); }, canClose: false, onEmptySelection: (emptySelection) { @@ -1746,9 +1748,10 @@ class _UploadReadyWidget extends StatelessWidget { driveDetailCubit: driveDetailCubit, justSelectName: true, onSelectionConfirmed: (name) { - context - .read() - .selectUndername(name.selectedName, name.selectedUndername); + // TODO: RE-ENABLE THIS + // context + // .read() + // .selectUndername(name.selectedName, name.selectedUndername); }, canClose: false, onEmptySelection: (emptySelection) { diff --git a/packages/ario_sdk/lib/src/ario_sdk.dart b/packages/ario_sdk/lib/src/ario_sdk.dart index 080cec456c..2442724722 100644 --- a/packages/ario_sdk/lib/src/ario_sdk.dart +++ b/packages/ario_sdk/lib/src/ario_sdk.dart @@ -1,7 +1,6 @@ library ario; import 'package:ario_sdk/ario_sdk.dart'; -import 'package:ario_sdk/src/models/response_object.dart'; abstract class ArioSDK { /// Get the list of available gateways @@ -28,6 +27,8 @@ abstract class ArioSDK { String undername = '@', }); + Future> getArNSNames(String address); + /// Get the primary name for the given address /// /// Throws [PrimaryNameNotFoundException] if the primary name is not found diff --git a/packages/ario_sdk/lib/src/implementations/ario_sdk_web.dart b/packages/ario_sdk/lib/src/implementations/ario_sdk_web.dart index 2a87f8a9b9..af959093e2 100644 --- a/packages/ario_sdk/lib/src/implementations/ario_sdk_web.dart +++ b/packages/ario_sdk/lib/src/implementations/ario_sdk_web.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'dart:js_util'; import 'package:ario_sdk/ario_sdk.dart'; -import 'package:ario_sdk/src/models/response_object.dart'; import 'package:js/js.dart'; class ArioSDKWeb implements ArioSDK { @@ -97,6 +96,33 @@ class ArioSDKWeb implements ArioSDK { return _setARNSImpl('', arnsUndername, true); } + @override + Future> getArNSNames(String address) async { + final processes = await _getARNSRecordsForWalletImpl(address); + + List names = []; + + for (var e in processes) { + final name = e.names[e.names.keys.first]; + final undernameLimit = name?.undernameLimit; + + if (undernameLimit == null) { + throw Exception('Under name limit is null'); + } + + e.state.records.removeWhere((key, value) => key == '@'); + + names.add(ArNSNameModel( + name: e.names.keys.first, + processId: e.names.keys.first, + records: e.state.records.length, + undernameLimit: undernameLimit, + )); + } + + return names; + } + @override Future getPrimaryNameDetails( String address, bool getLogo) async { diff --git a/packages/ario_sdk/lib/src/implementations/ario_sdk_web_stub.dart b/packages/ario_sdk/lib/src/implementations/ario_sdk_web_stub.dart index fd9425bca2..d2ecff52ab 100644 --- a/packages/ario_sdk/lib/src/implementations/ario_sdk_web_stub.dart +++ b/packages/ario_sdk/lib/src/implementations/ario_sdk_web_stub.dart @@ -1,5 +1,4 @@ import 'package:ario_sdk/ario_sdk.dart'; -import 'package:ario_sdk/src/models/response_object.dart'; class ArioSDKWeb implements ArioSDK { @override @@ -44,6 +43,12 @@ class ArioSDKWeb implements ArioSDK { throw UnimplementedError(); } + @override + Future> getArNSNames(String address) { + // TODO: implement getArNSNames + throw UnimplementedError(); + } + @override Future getPrimaryNameDetails( String address, diff --git a/packages/ario_sdk/lib/src/models/arns_name_model.dart b/packages/ario_sdk/lib/src/models/arns_name_model.dart new file mode 100644 index 0000000000..0d41b5c929 --- /dev/null +++ b/packages/ario_sdk/lib/src/models/arns_name_model.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +class ArNSNameModel extends Equatable { + // TODO: maybe a list of records + final String name; + final String processId; + final int records; + final int undernameLimit; + + const ArNSNameModel({ + required this.name, + required this.processId, + required this.records, + required this.undernameLimit, + }); + + @override + List get props => [name, processId, records, undernameLimit]; + + @override + String toString() { + return 'ArNSNameModel(name: $name, processId: $processId, records: $records, undernameLimit: $undernameLimit)'; + } +} diff --git a/packages/ario_sdk/lib/src/models/models.dart b/packages/ario_sdk/lib/src/models/models.dart index c80d8987e4..500446b302 100644 --- a/packages/ario_sdk/lib/src/models/models.dart +++ b/packages/ario_sdk/lib/src/models/models.dart @@ -1,5 +1,7 @@ export 'ant_record.dart'; +export 'arns_name_model.dart'; export 'arns_record.dart'; export 'gateway.dart'; +export 'response_object.dart'; export 'undername.dart'; export 'primary_name_details.dart'; diff --git a/packages/ario_sdk/lib/src/models/response_object.dart b/packages/ario_sdk/lib/src/models/response_object.dart index 68d6cc54f3..2ed99d3e7b 100644 --- a/packages/ario_sdk/lib/src/models/response_object.dart +++ b/packages/ario_sdk/lib/src/models/response_object.dart @@ -28,14 +28,14 @@ class ResponseObject { } class ARNSProcessData { - final State state; + final ProcessState state; final Map names; ARNSProcessData({required this.state, required this.names}); factory ARNSProcessData.fromJson(Map json) { return ARNSProcessData( - state: State.fromJson(json['state']), + state: ProcessState.fromJson(json['state']), names: (json['names'] as Map).map( (key, value) => MapEntry(key, ARNSName.fromJson(value)), ), @@ -50,7 +50,7 @@ class ARNSProcessData { } } -class State { +class ProcessState { final int totalSupply; final String? sourceCodeTxId; final Map balances; @@ -63,7 +63,7 @@ class State { final String name; final String owner; - State({ + ProcessState({ required this.totalSupply, this.sourceCodeTxId, required this.balances, @@ -77,8 +77,8 @@ class State { required this.owner, }); - factory State.fromJson(Map json) { - return State( + factory ProcessState.fromJson(Map json) { + return ProcessState( totalSupply: json['TotalSupply'], sourceCodeTxId: json['Source-Code-TX-ID'], balances: Map.from(json['Balances']), diff --git a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart index e3375298a2..e82b4e0ff2 100644 --- a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart +++ b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart @@ -1,451 +1,217 @@ -import 'package:ardrive/arns/domain/arns_repository.dart'; import 'package:ardrive/arns/presentation/assign_name_bloc/assign_name_bloc.dart'; -import 'package:ardrive/pages/drive_detail/models/data_table_item.dart'; -import 'package:ario_sdk/ario_sdk.dart'; +import 'package:ario_sdk/ario_sdk.dart' as sdk; import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import '../../../test_utils/utils.dart'; - -class MockARNSRepository extends Mock implements ARNSRepository {} - -class MockFileDataTableItem extends Mock implements FileDataTableItem {} +import '../../../user/name/presentation/bloc/profile_name_bloc_test.dart'; void main() { + late MockArDriveAuth auth; + late MockARNSRepository arnsRepository; + setUpAll(() { - registerFallbackValue(const ARNSUndername( - name: 'test_undername', - domain: 'test.ar', - record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), - )); + auth = MockArDriveAuth(); + arnsRepository = MockARNSRepository(); + registerFallbackValue( - const ANTRecord(domain: 'test.ar', processId: 'test_process_id')); + const sdk.ANTRecord( + domain: 'test-domain', + processId: 'test-process-id', + ), + ); }); group('AssignNameBloc', () { - late AssignNameBloc assignNameBloc; - late MockARNSRepository mockArnsRepository; - late MockArDriveAuth mockAuth; - late MockFileDataTableItem mockFileDataTableItem; - - setUp(() { - mockArnsRepository = MockARNSRepository(); - mockAuth = MockArDriveAuth(); - mockFileDataTableItem = MockFileDataTableItem(); - - assignNameBloc = AssignNameBloc( - auth: mockAuth, - fileDataTableItem: mockFileDataTableItem, - arnsRepository: mockArnsRepository, - ); - }); - - tearDown(() { - assignNameBloc.close(); - }); - group('LoadNames', () { - test('emits [LoadingNames, NamesLoaded] when LoadNames is added', - () async { - // Arrange - const walletAddress = 'test_wallet_address'; - final antRecords = [ - const ANTRecord(domain: 'test1.ar', processId: 'process1'), - const ANTRecord(domain: 'test2.ar', processId: 'process2'), - ]; - - when(() => mockAuth.getWalletAddress()) - .thenAnswer((_) async => walletAddress); - when(() => mockArnsRepository.getAntRecordsForWallet(walletAddress)) - .thenAnswer((_) async => antRecords); - - // Act - assignNameBloc.add(const LoadNames()); - - // Assert - await expectLater( - assignNameBloc.stream, - emitsInOrder([ - isA(), - isA() - .having((state) => state.names, 'names', antRecords), - ]), - ); - - verify(() => mockAuth.getWalletAddress()).called(1); - verify(() => mockArnsRepository.getAntRecordsForWallet( - walletAddress, - )).called(1); - }); - - test( - 'emits [LoadingNames, AssignNameEmptyState] when LoadNames is added and no names are returned', - () async { - // Arrange - const walletAddress = 'test_wallet_address'; - final antRecords = []; - - when(() => mockAuth.getWalletAddress()) - .thenAnswer((_) async => walletAddress); - when(() => mockArnsRepository.getAntRecordsForWallet(walletAddress)) - .thenAnswer((_) async => antRecords); - - // Act - assignNameBloc.add(const LoadNames()); - - // Assert - await expectLater( - assignNameBloc.stream, - emitsInOrder([ - isA(), - isA(), - ]), - ); - - verify(() => mockAuth.getWalletAddress()).called(1); - verify(() => mockArnsRepository.getAntRecordsForWallet( - walletAddress, - )).called(1); - }); - - test('emits [LoadingNames, LoadingNamesFailed] when LoadNames is added', - () async { - // Arrange - const walletAddress = 'test_wallet_address'; - - when(() => mockAuth.getWalletAddress()) - .thenAnswer((_) async => walletAddress); - when(() => mockArnsRepository.getAntRecordsForWallet(walletAddress, - update: true)).thenThrow(StateError('Test error')); - - // Act - assignNameBloc.add(const LoadNames()); - - // Assert - await expectLater( - assignNameBloc.stream, - emitsInOrder([ - isA(), - isA(), - ]), - ); - }); - }); - - group('SelectName', () { - test('emits NamesLoaded with selected name when SelectName is added', - () async { - // Arrange - final antRecords = [ - const ANTRecord(domain: 'domain1.ar', processId: 'process1'), - const ANTRecord(domain: 'domain2.ar', processId: 'process2'), - ]; - final selectedName = antRecords[0]; - - assignNameBloc.emit(NamesLoaded(names: antRecords)); - - // Act - assignNameBloc.add(SelectName(selectedName)); - - // Assert - await expectLater( - assignNameBloc.stream, - emits( - isA() - .having((state) => state.names, 'names', antRecords) - .having((state) => state.selectedName, 'selectedName', - selectedName), + blocTest( + 'emits [LoadingNames, NamesLoaded] when names are loaded successfully', + setUp: () { + when(() => auth.getWalletAddress()) + .thenAnswer((_) async => 'test-wallet-address'); + when(() => + arnsRepository.getAntRecordsForWallet(any(), update: false)) + .thenAnswer((_) async => [ + const sdk.ANTRecord( + domain: 'test.ar', + processId: 'process-id', + ) + ]); + when(() => arnsRepository.getARNSNameModelsForWallet(any())) + .thenAnswer((_) async => [ + const sdk.ArNSNameModel( + name: 'test.ar', + processId: 'process-id', + records: 1, + undernameLimit: 100, + ) + ]); + }, + build: () => AssignNameBloc( + auth: auth, + arnsRepository: arnsRepository, + ), + act: (bloc) => bloc.add(const LoadNames()), + expect: () => [ + isA(), + isA().having( + (state) => state.nameModels.first.name, + 'name', + 'test.ar', ), - ); - }); - - test( - 'emits NamesLoaded when SelectName is added in UndernamesLoaded state', - () async { - // Arrange - final antRecords = [ - const ANTRecord(domain: 'domain1.ar', processId: 'process1'), - const ANTRecord(domain: 'domain2.ar', processId: 'process2'), - ]; - final selectedName = antRecords[1]; - final undernames = [ - const ARNSUndername( - name: 'undername1', - domain: 'domain1.ar', - record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600)), - ]; - - assignNameBloc.emit(UndernamesLoaded( - names: antRecords, - selectedName: antRecords[0], - undernames: undernames, - selectedUndername: null, - )); + ], + ); - // Act - assignNameBloc.add(SelectName(selectedName)); + blocTest( + 'emits [LoadingNames, AssignNameEmptyState] when no names are found', + setUp: () { + when(() => auth.getWalletAddress()) + .thenAnswer((_) async => 'test-wallet-address'); + when(() => + arnsRepository.getAntRecordsForWallet(any(), update: false)) + .thenAnswer((_) async => []); + when(() => arnsRepository.getARNSNameModelsForWallet(any())) + .thenAnswer((_) async => []); + }, + build: () => AssignNameBloc( + auth: auth, + arnsRepository: arnsRepository, + ), + act: (bloc) => bloc.add(const LoadNames()), + expect: () => [ + isA(), + isA(), + ], + ); - // Assert - await expectLater( - assignNameBloc.stream, - emits( - isA() - .having((state) => state.names, 'names', antRecords) - .having((state) => state.selectedName, 'selectedName', - selectedName), - ), - ); - }); + blocTest( + 'emits [LoadingNames, LoadingNamesFailed] when error occurs', + setUp: () { + when(() => auth.getWalletAddress()) + .thenThrow(Exception('Failed to get wallet address')); + }, + build: () => AssignNameBloc( + auth: auth, + arnsRepository: arnsRepository, + ), + act: (bloc) => bloc.add(const LoadNames()), + expect: () => [ + isA(), + isA(), + ], + ); }); - group('LoadUndernames', () { - test( - 'emits LoadingUndernames and then UndernamesLoaded when LoadUndernames is added', - () async { - // Arrange - final antRecords = [ - const ANTRecord(domain: 'domain1.ar', processId: 'process1'), - const ANTRecord(domain: 'domain2.ar', processId: 'process2'), - ]; - final selectedName = antRecords[0]; - final undernames = [ - const ARNSUndername( - name: 'undername1', - domain: 'domain1.ar', - record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), - ), - const ARNSUndername( - name: 'undername2', - domain: 'domain1.ar', - record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), - ), - ]; - - when(() => mockArnsRepository.getARNSUndernames(selectedName)) - .thenAnswer((_) async => undernames); - - assignNameBloc.add(SelectName(selectedName)); - assignNameBloc - .emit(NamesLoaded(names: antRecords, selectedName: selectedName)); - - // Act - assignNameBloc.add(const LoadUndernames()); - - // Assert - await expectLater( - assignNameBloc.stream, - emitsInOrder([ - isA(), - isA() - .having((state) => state.names, 'names', antRecords) - .having( - (state) => state.selectedName, 'selectedName', selectedName) - .having((state) => state.undernames, 'undernames', undernames) - .having((state) => state.selectedUndername, 'selectedUndername', - null), - ]), - ); - - verify(() => mockArnsRepository.getARNSUndernames(selectedName)) - .called(1); + group('SelectName', () { + setUp(() { + when(() => auth.getWalletAddress()) + .thenAnswer((_) async => 'test-wallet-address'); + when(() => arnsRepository.getAntRecordsForWallet(any(), update: false)) + .thenAnswer((_) async => [ + const sdk.ANTRecord( + domain: 'test.ar', + processId: 'process-id', + ) + ]); + + when(() => arnsRepository.getARNSNameModelsForWallet(any())) + .thenAnswer((_) async => [ + const sdk.ArNSNameModel( + name: 'test.ar', + processId: 'process-id', + records: 1, + undernameLimit: 100, + ) + ]); }); - }); - - group('SelectUndername', () { - test( - 'emits UndernamesLoaded with selected undername when SelectUndername is added', - () async { - // Arrange - final antRecords = [ - const ANTRecord(domain: 'domain1.ar', processId: 'process1'), - const ANTRecord(domain: 'domain2.ar', processId: 'process2'), - ]; - final selectedName = antRecords[0]; - final undernames = [ - const ARNSUndername( - name: 'undername1', - domain: 'domain1.ar', - record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), - ), - const ARNSUndername( - name: 'undername2', - domain: 'domain1.ar', - record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), - ), - ]; - final selectedUndername = undernames[0]; - - assignNameBloc.emit(UndernamesLoaded( - names: antRecords, - selectedName: selectedName, - undernames: undernames, - selectedUndername: null, - )); - - // Act - assignNameBloc.add(SelectUndername(undername: selectedUndername)); - - // Assert - await expectLater( - assignNameBloc.stream, - emits( - isA() - .having((state) => state.names, 'names', antRecords) - .having( - (state) => state.selectedName, 'selectedName', selectedName) - .having((state) => state.undernames, 'undernames', undernames) - .having((state) => state.selectedUndername, 'selectedUndername', - selectedUndername), + blocTest( + 'emits [LoadingNames, NamesLoaded] when loading names succeeds', + build: () => AssignNameBloc( + auth: auth, + arnsRepository: arnsRepository, + )..add(const LoadNames()), + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add( + const SelectName( + sdk.ArNSNameModel( + name: 'test.ar', + processId: 'process-id', + records: 1, + undernameLimit: 100, + ), + ), + ); + }, + expect: () => [ + isA(), + isA(), + isA().having( + (state) => state.selectedName?.name, + 'name', + 'test.ar', ), - ); - }); - }); + ], + ); - group('ConfirmSelection', () { blocTest( - 'emits [ConfirmingSelection, SelectionConfirmed] when ConfirmSelection is added', - build: () { - when(() => mockFileDataTableItem.dataTxId) - .thenReturn('test_data_tx_id'); - when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); - when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); - final antRecords = [ - const ANTRecord(domain: 'test1.ar', processId: 'process1'), - const ANTRecord(domain: 'test2.ar', processId: 'process2'), - ]; - - const walletAddress = 'test_wallet_address'; - - when(() => mockAuth.getWalletAddress()) - .thenAnswer((_) async => walletAddress); - when(() => mockArnsRepository.getAntRecordsForWallet(walletAddress)) - .thenAnswer((_) async => antRecords); - when(() => mockArnsRepository.setUndernamesToFile( - undername: any(named: 'undername'), - fileId: any(named: 'fileId'), - driveId: any(named: 'driveId'), - processId: any(named: 'processId'), - )).thenAnswer((_) async {}); - when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( - (_) async => [ - const ARNSUndername( - name: 'undername', - domain: 'domain', - record: - ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), - ) - ], - ); - return assignNameBloc; + 'emits [LoadingNames, NamesLoaded, LoadingUndernames, UndernamesLoaded] when loading undernames succeeds', + build: () => AssignNameBloc( + auth: auth, + arnsRepository: arnsRepository, + )..add(const LoadNames()), + setUp: () { + when(() => arnsRepository.getARNSUndernames(any())) + .thenAnswer((_) async => [ + const sdk.ARNSUndername( + domain: 'test.ar', + name: 'test.ar', + record: sdk.ARNSRecord( + transactionId: 'transaction-id', + ttlSeconds: 100, + ), + ) + ]); }, - act: (bloc) { - bloc.add(const LoadNames()); + act: (bloc) async { + await Future.delayed(const Duration(milliseconds: 100)); bloc.add( - const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); + const SelectName( + sdk.ArNSNameModel( + name: 'test.ar', + processId: 'process-id', + records: 1, + undernameLimit: 100, + ), + ), + ); + await Future.delayed(const Duration(milliseconds: 100)); bloc.add(const LoadUndernames()); - bloc.add(const SelectUndername( - undername: ARNSUndername( - name: 'undername', - domain: 'domain', - record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), + await Future.delayed(const Duration(milliseconds: 100)); + bloc.add( + const SelectName( + sdk.ArNSNameModel( + name: 'test.ar', + processId: 'process-id', + records: 1, + undernameLimit: 100, + ), ), - )); - bloc.add(ConfirmSelectionAndUpload()); + ); }, expect: () => [ isA(), isA(), + isA(), isA(), isA(), - isA(), - isA(), - isA() - .having((state) => state.address, 'address', - 'https://undername_domain.ar-io.dev') - .having((state) => state.arAddress, 'arAddress', - 'ar://undername_domain'), + isA().having( + (state) => state.selectedName?.name, + 'name', + 'test.ar', + ), ], - verify: (_) { - verify(() => mockArnsRepository.setUndernamesToFile( - undername: any(named: 'undername'), - fileId: any(named: 'fileId'), - driveId: any(named: 'driveId'), - processId: any(named: 'processId'), - )).called(1); - }, ); }); - - blocTest( - 'emits [ConfirmingSelection, SelectionFailed] when ConfirmSelection is added', - build: () { - when(() => mockFileDataTableItem.dataTxId) - .thenReturn('test_data_tx_id'); - when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); - when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); - final antRecords = [ - const ANTRecord(domain: 'test1.ar', processId: 'process1'), - const ANTRecord(domain: 'test2.ar', processId: 'process2'), - ]; - - const walletAddress = 'test_wallet_address'; - - when(() => mockAuth.getWalletAddress()) - .thenAnswer((_) async => walletAddress); - when(() => mockArnsRepository.getAntRecordsForWallet( - walletAddress, - )).thenAnswer((_) async => antRecords); - - /// FAILED CALL - when(() => mockArnsRepository.setUndernamesToFile( - undername: any(named: 'undername'), - fileId: any(named: 'fileId'), - driveId: any(named: 'driveId'), - processId: any(named: 'processId'), - )).thenThrow(StateError('Test error')); - when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( - (_) async => [ - const ARNSUndername( - name: 'undername', - domain: 'domain', - record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), - ) - ], - ); - return assignNameBloc; - }, - act: (bloc) { - bloc.add(const LoadNames()); - bloc.add( - const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); - bloc.add(const LoadUndernames()); - bloc.add(const SelectUndername( - undername: ARNSUndername( - name: 'undername', - domain: 'domain', - record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), - ), - )); - bloc.add(ConfirmSelectionAndUpload()); - }, - expect: () => [ - isA(), - isA(), - isA(), - isA(), - isA(), - isA(), - isA(), - ], - verify: (_) { - verify(() => mockArnsRepository.setUndernamesToFile( - undername: any(named: 'undername'), - fileId: any(named: 'fileId'), - driveId: any(named: 'driveId'), - processId: any(named: 'processId'), - )).called(1); - }, - ); }); }