diff --git a/android/fastlane/metadata/android/en-US/changelogs/62.txt b/android/fastlane/metadata/android/en-US/changelogs/62.txt new file mode 100644 index 0000000000..7ce29efd10 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/62.txt @@ -0,0 +1,3 @@ +- Adds an explanation for the privacy of drives on the Create Drive Modal +- Starts tagging public pins with new GQL Tags: ArFS-Pin and Pinned-Data-Tx +- Updates the icon for the Detach Drive option diff --git a/lib/blocs/drive_create/drive_create_cubit.dart b/lib/blocs/drive_create/drive_create_cubit.dart index 208c10a5da..7050d2e5f7 100644 --- a/lib/blocs/drive_create/drive_create_cubit.dart +++ b/lib/blocs/drive_create/drive_create_cubit.dart @@ -1,5 +1,9 @@ import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/core/arfs/entities/arfs_entities.dart' + show DrivePrivacy; +import 'package:ardrive/entities/constants.dart' as constants; +import 'package:ardrive/entities/drive_entity.dart'; +import 'package:ardrive/entities/folder_entity.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; @@ -15,7 +19,9 @@ part 'drive_create_state.dart'; class DriveCreateCubit extends Cubit { final form = FormGroup({ 'privacy': FormControl( - value: DrivePrivacy.private, validators: [Validators.required]), + value: DrivePrivacy.private.name, + validators: [Validators.required], + ), }); final ArweaveService _arweave; @@ -35,25 +41,32 @@ class DriveCreateCubit extends Cubit { _driveDao = driveDao, _profileCubit = profileCubit, _drivesCubit = drivesCubit, - super(DriveCreateInitial()); + super(const DriveCreateInitial(privacy: DrivePrivacy.private)); + + void onPrivacyChanged() { + final privacy = form.control('privacy').value == DrivePrivacy.private.name + ? DrivePrivacy.private + : DrivePrivacy.public; + emit(state.copyWith(privacy: privacy)); + } Future submit( String driveName, ) async { final profile = _profileCubit.state as ProfileLoggedIn; if (await _profileCubit.logoutIfWalletMismatch()) { - emit(DriveCreateWalletMismatch()); + emit(DriveCreateWalletMismatch(privacy: state.privacy)); return; } final minimumWalletBalance = BigInt.from(10000000); if (profile.walletBalance <= minimumWalletBalance && !_turboUploadService.useTurboUpload) { - emit(DriveCreateZeroBalance()); + emit(DriveCreateZeroBalance(privacy: state.privacy)); return; } - emit(DriveCreateInProgress()); + emit(DriveCreateInProgress(privacy: state.privacy)); try { final String drivePrivacy = form.control('privacy').value; @@ -72,8 +85,8 @@ class DriveCreateCubit extends Cubit { name: driveName, rootFolderId: createRes.rootFolderId, privacy: drivePrivacy, - authMode: drivePrivacy == DrivePrivacy.private - ? DriveAuthMode.password + authMode: drivePrivacy == constants.DrivePrivacy.private + ? constants.DriveAuthMode.password : null, ); @@ -140,12 +153,12 @@ class DriveCreateCubit extends Cubit { addError(err); } - emit(DriveCreateSuccess()); + emit(DriveCreateSuccess(privacy: state.privacy)); } @override void onError(Object error, StackTrace stackTrace) { - emit(DriveCreateFailure()); + emit(DriveCreateFailure(privacy: state.privacy)); super.onError(error, stackTrace); logger.e('Failed to create drive', error, stackTrace); diff --git a/lib/blocs/drive_create/drive_create_state.dart b/lib/blocs/drive_create/drive_create_state.dart index ac3d115a4e..7c07247a49 100644 --- a/lib/blocs/drive_create/drive_create_state.dart +++ b/lib/blocs/drive_create/drive_create_state.dart @@ -2,18 +2,68 @@ part of 'drive_create_cubit.dart'; @immutable abstract class DriveCreateState extends Equatable { + final DrivePrivacy privacy; + + const DriveCreateState({required this.privacy}); + + DriveCreateState copyWith({DrivePrivacy? privacy}) { + throw UnimplementedError(); + } + @override - List get props => []; + List get props => [privacy]; } -class DriveCreateInitial extends DriveCreateState {} +class DriveCreateInitial extends DriveCreateState { + const DriveCreateInitial({required super.privacy}); -class DriveCreateZeroBalance extends DriveCreateState {} + @override + DriveCreateInitial copyWith({DrivePrivacy? privacy}) { + return DriveCreateInitial(privacy: privacy ?? this.privacy); + } +} -class DriveCreateInProgress extends DriveCreateState {} +class DriveCreateZeroBalance extends DriveCreateState { + const DriveCreateZeroBalance({required super.privacy}); -class DriveCreateSuccess extends DriveCreateState {} + @override + DriveCreateZeroBalance copyWith({DrivePrivacy? privacy}) { + return DriveCreateZeroBalance(privacy: privacy ?? this.privacy); + } +} + +class DriveCreateInProgress extends DriveCreateState { + const DriveCreateInProgress({required super.privacy}); + + @override + DriveCreateInProgress copyWith({DrivePrivacy? privacy}) { + return DriveCreateInProgress(privacy: privacy ?? this.privacy); + } +} -class DriveCreateFailure extends DriveCreateState {} +class DriveCreateSuccess extends DriveCreateState { + const DriveCreateSuccess({required super.privacy}); -class DriveCreateWalletMismatch extends DriveCreateState {} + @override + DriveCreateSuccess copyWith({DrivePrivacy? privacy}) { + return DriveCreateSuccess(privacy: privacy ?? this.privacy); + } +} + +class DriveCreateFailure extends DriveCreateState { + const DriveCreateFailure({required super.privacy}); + + @override + DriveCreateFailure copyWith({DrivePrivacy? privacy}) { + return DriveCreateFailure(privacy: privacy ?? this.privacy); + } +} + +class DriveCreateWalletMismatch extends DriveCreateState { + const DriveCreateWalletMismatch({required super.privacy}); + + @override + DriveCreateWalletMismatch copyWith({DrivePrivacy? privacy}) { + return DriveCreateWalletMismatch(privacy: privacy ?? this.privacy); + } +} diff --git a/lib/blocs/pin_file/pin_file_bloc.dart b/lib/blocs/pin_file/pin_file_bloc.dart index b1ffddb6d7..257fdffb0a 100644 --- a/lib/blocs/pin_file/pin_file_bloc.dart +++ b/lib/blocs/pin_file/pin_file_bloc.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/crypto/crypto.dart'; -import 'package:ardrive/entities/file_entity.dart'; +import 'package:ardrive/entities/entities.dart' show EntityTag, FileEntity; import 'package:ardrive/entities/string_types.dart'; import 'package:ardrive/misc/misc.dart'; import 'package:ardrive/models/models.dart'; @@ -279,6 +279,7 @@ class PinFileBloc extends Bloc { ) async { final stateAsPinFileFieldsValid = state as PinFileFieldsValid; final profileState = _profileCubit.state as ProfileLoggedIn; + final wallet = profileState.wallet; emit(PinFileCreating( id: stateAsPinFileFieldsValid.id, @@ -311,13 +312,29 @@ class PinFileBloc extends Bloc { .folderById(driveId: _driveId, folderId: _parentFolderId) .getSingle(); + final isAPublicPin = fileKey == null; + if (_turboUploadService.useTurboUpload) { final fileDataItem = await _arweave.prepareEntityDataItem( newFileEntity, - profileState.wallet, + wallet, key: fileKey, + skipSignature: true, ); + if (isAPublicPin) { + fileDataItem.addTag( + EntityTag.arFsPin, + 'true', + ); + fileDataItem.addTag( + EntityTag.pinnedDataTx, + newFileEntity.dataTxId!, + ); + } + + await fileDataItem.sign(wallet); + await _turboUploadService.postDataItem( dataItem: fileDataItem, wallet: profileState.wallet, @@ -326,10 +343,24 @@ class PinFileBloc extends Bloc { } else { final fileDataItem = await _arweave.prepareEntityTx( newFileEntity, - profileState.wallet, + wallet, fileKey, + skipSignature: true, ); + if (isAPublicPin) { + fileDataItem.addTag( + EntityTag.arFsPin, + 'true', + ); + fileDataItem.addTag( + EntityTag.pinnedDataTx, + newFileEntity.dataTxId!, + ); + } + + await fileDataItem.sign(wallet); + await _arweave.postTx(fileDataItem); newFileEntity.txId = fileDataItem.id; } diff --git a/lib/components/drive_create_form.dart b/lib/components/drive_create_form.dart index 0645db6002..5800b22c93 100644 --- a/lib/components/drive_create_form.dart +++ b/lib/components/drive_create_form.dart @@ -1,4 +1,5 @@ import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; @@ -73,6 +74,8 @@ class _DriveCreateFormState extends State { ], ); } else { + final privacy = state.privacy; + return ArDriveStandardModal( title: appLocalizationsOf(context).createDriveEmphasized, content: SizedBox( @@ -108,24 +111,43 @@ class _DriveCreateFormState extends State { ReactiveDropdownField( formControlName: 'privacy', decoration: InputDecoration( - labelText: appLocalizationsOf(context).privacy), + labelText: appLocalizationsOf(context).privacy, + ), showErrors: (control) => control.dirty && control.invalid, validationMessages: kValidationMessages(appLocalizationsOf(context)), items: [ DropdownMenuItem( - value: 'public', + value: DrivePrivacy.public.name, child: Text(appLocalizationsOf(context).public), ), DropdownMenuItem( - value: 'private', + value: DrivePrivacy.private.name, child: Text( appLocalizationsOf(context).private, ), ) ], + onChanged: (_) { + context.read().onPrivacyChanged(); + }, ), + const SizedBox(height: 32), + Row(children: [ + if (privacy == DrivePrivacy.private) + Flexible( + child: Text( + appLocalizationsOf(context) + .drivePrivacyDescriptionPrivate, + )) + else + Flexible( + child: Text( + appLocalizationsOf(context) + .drivePrivacyDescriptionPublic, + )) + ]), ], ), ), diff --git a/lib/entities/constants.dart b/lib/entities/constants.dart index 45089943d8..8d904258c7 100644 --- a/lib/entities/constants.dart +++ b/lib/entities/constants.dart @@ -30,6 +30,9 @@ class EntityTag { static const blockEnd = 'Block-End'; static const dataStart = 'Data-Start'; static const dataEnd = 'Data-End'; + + static const pinnedDataTx = 'Pinned-Data-Tx'; + static const arFsPin = 'ArFS-Pin'; } class ContentType { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0619f9786b..a3e2c9823a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -519,6 +519,14 @@ "@driveName": { "description": "The name of certain drive" }, + "drivePrivacyDescriptionPrivate": "Private Drives offer state-of-the-art security, you control access.", + "@drivePrivacyDescriptionPrivate": { + "description": "Explains private drives." + }, + "drivePrivacyDescriptionPublic": "Public Drives are discoverable, others can find and view the contents.", + "@drivePrivacyDescriptionPublic": { + "description": "Explains public drives." + }, "driveRoot": "Drive root", "@driveRoot": { "description": "The folder entity that is at the top of the folder hierarchy" diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 06fb51832d..1aa1e3b1d7 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -379,9 +379,10 @@ class _DriveDetailPageState extends State { ); }, content: _buildItem( - appLocalizationsOf(context) - .detachDrive, - ArDriveIcons.triangle()), + appLocalizationsOf(context) + .detachDrive, + ArDriveIcons.detach(), + ), ), ], child: HoverWidget( diff --git a/lib/services/arweave/arweave_service.dart b/lib/services/arweave/arweave_service.dart index 06ad23c6f2..4afa342aaf 100644 --- a/lib/services/arweave/arweave_service.dart +++ b/lib/services/arweave/arweave_service.dart @@ -1068,11 +1068,14 @@ class ArweaveService { Entity entity, Wallet wallet, { SecretKey? key, + bool skipSignature = false, }) async { final item = await entity.asDataItem(key); item.setOwner(await wallet.getOwner()); - await item.sign(wallet); + if (!skipSignature) { + await item.sign(wallet); + } return item; } diff --git a/pubspec.lock b/pubspec.lock index aeb6483b82..8216f3c13f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -79,11 +79,11 @@ packages: dependency: "direct main" description: path: "." - ref: "v1.9.1" - resolved-ref: c240bab3c61efec613832ce5636226aff2a18d63 + ref: "v1.9.2" + resolved-ref: f8fb1bf502541e46caeb6e7e6270e2c488d7e126 url: "https://github.com/ar-io/ardrive_ui.git" source: git - version: "1.9.1" + version: "1.9.2" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a075f39234..8f6d51a681 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.13.0 +version: 2.14.0 environment: sdk: '>=2.18.5 <3.0.0' @@ -39,7 +39,7 @@ dependencies: ardrive_ui: git: url: https://github.com/ar-io/ardrive_ui.git - ref: v1.9.1 + ref: v1.9.2 artemis: ^7.0.0-beta.13 arweave: git: diff --git a/test/blocs/drive_create_cubit_test.dart b/test/blocs/drive_create_cubit_test.dart index 97c7694b6b..8ff7e10435 100644 --- a/test/blocs/drive_create_cubit_test.dart +++ b/test/blocs/drive_create_cubit_test.dart @@ -1,12 +1,11 @@ @Tags(['broken']) import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/core/crypto/crypto.dart'; -import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; +import 'package:ardrive/entities/entity.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; -import 'package:ardrive/utils/app_flavors.dart'; import 'package:ardrive/utils/app_platform.dart'; import 'package:arweave/arweave.dart'; import 'package:bloc_test/bloc_test.dart'; @@ -19,109 +18,139 @@ import 'package:test/test.dart'; import '../test_utils/fakes.dart'; import '../test_utils/utils.dart'; +class FakeEntity extends Fake implements Entity {} + void main() { - group('DriveCreateCubit', () { - late Database db; - late DriveDao driveDao; - - late ArweaveService arweave; - late TurboUploadService turboUploadService; - late DrivesCubit drivesCubit; - late ProfileCubit profileCubit; - late DriveCreateCubit driveCreateCubit; - - const validDriveName = 'valid-drive-name'; - - setUp(() async { - registerFallbackValue(DrivesStateFake()); - registerFallbackValue(ProfileStateFake()); - - db = getTestDb(); - driveDao = db.driveDao; - final configService = ConfigService( - appFlavors: AppFlavors(MockEnvFetcher()), - configFetcher: MockConfigFetcher()); - final config = await configService.loadConfig(); - - AppPlatform.setMockPlatform(platform: SystemPlatform.unknown); - arweave = ArweaveService( - Arweave(gatewayUrl: Uri.parse(config.defaultArweaveGatewayUrl!)), - ArDriveCrypto(), + group( + 'DriveCreateCubit', + () { + late Database db; + late DriveDao driveDao; + + late ArweaveService arweave; + late TurboUploadService turboUploadService; + late DrivesCubit drivesCubit; + late ProfileCubit profileCubit; + late DriveCreateCubit driveCreateCubit; + + late Wallet wallet; + + const validDriveName = 'valid-drive-name'; + + setUp(() async { + wallet = getTestWallet(); + + registerFallbackValue(DrivesStateFake()); + registerFallbackValue(ProfileStateFake()); + registerFallbackValue(DataBundle(blob: Uint8List.fromList([]))); + registerFallbackValue(wallet); + registerFallbackValue(FakeEntity()); + + db = getTestDb(); + driveDao = db.driveDao; + AppPlatform.setMockPlatform(platform: SystemPlatform.unknown); + arweave = MockArweaveService(); + turboUploadService = DontUseUploadService(); + drivesCubit = MockDrivesCubit(); + profileCubit = MockProfileCubit(); + + final walletAddress = await wallet.getAddress(); + final walletOwner = await wallet.getAddress(); + + final keyBytes = Uint8List(32); + fillBytesWithSecureRandom(keyBytes); + + when(() => profileCubit.state).thenReturn( + ProfileLoggedIn( + username: 'Test', + password: '123', + wallet: wallet, + walletAddress: walletAddress, + walletBalance: BigInt.from(10000001), + cipherKey: SecretKey(keyBytes), + useTurbo: turboUploadService.useTurboUpload, + ), + ); + + when(() => profileCubit.logoutIfWalletMismatch()).thenAnswer( + (invocation) => Future.value(false), + ); + + when(() => arweave.prepareBundledDataItem(any(), any())).thenAnswer( + (invocation) => Future.value( + DataItem.withBlobData( + owner: walletOwner, + data: Uint8List.fromList([]), + ), + ), + ); + + when(() => arweave.prepareEntityDataItem(any(), any())).thenAnswer( + (invocation) => Future.value( + DataItem.withBlobData( + owner: walletOwner, + data: Uint8List.fromList([]), + ), + ), + ); + + driveCreateCubit = DriveCreateCubit( + arweave: arweave, + turboUploadService: turboUploadService, + driveDao: driveDao, + drivesCubit: drivesCubit, + profileCubit: profileCubit, + ); + }); + + tearDown(() async { + await db.close(); + }); + + blocTest( + 'create public drive', + build: () => driveCreateCubit, + act: (bloc) async { + bloc.form.value = { + 'name': validDriveName, + 'privacy': DrivePrivacy.public.name, + }; + await bloc.submit(''); + }, + expect: () => [ + const DriveCreateInProgress(privacy: DrivePrivacy.public), + const DriveCreateSuccess(privacy: DrivePrivacy.public), + ], + verify: (_) {}, ); - turboUploadService = DontUseUploadService(); - drivesCubit = MockDrivesCubit(); - profileCubit = MockProfileCubit(); - - final wallet = getTestWallet(); - final walletAddress = await wallet.getAddress(); - - final keyBytes = Uint8List(32); - fillBytesWithSecureRandom(keyBytes); - - when(() => profileCubit.state).thenReturn( - ProfileLoggedIn( - username: 'Test', - password: '123', - wallet: wallet, - walletAddress: walletAddress, - walletBalance: BigInt.one, - cipherKey: SecretKey(keyBytes), - useTurbo: turboUploadService.useTurboUpload, - ), + + blocTest( + 'create private drive', + build: () => driveCreateCubit, + act: (bloc) async { + bloc.form.value = { + 'name': validDriveName, + 'privacy': DrivePrivacy.private.name, + }; + + bloc.onPrivacyChanged(); + + await bloc.submit(''); + }, + expect: () => [ + const DriveCreateInProgress(privacy: DrivePrivacy.public), + const DriveCreateInProgress(privacy: DrivePrivacy.private), + const DriveCreateSuccess(privacy: DrivePrivacy.private), + ], + verify: (_) {}, ); - driveCreateCubit = DriveCreateCubit( - arweave: arweave, - turboUploadService: turboUploadService, - driveDao: driveDao, - drivesCubit: drivesCubit, - profileCubit: profileCubit, + blocTest( + 'does nothing when submitted without valid form', + build: () => driveCreateCubit, + act: (bloc) => bloc.submit(''), + expect: () => [], ); - }); - - tearDown(() async { - await db.close(); - }); - - blocTest( - 'create public drive', - build: () => driveCreateCubit, - act: (bloc) async { - bloc.form.value = { - 'name': validDriveName, - 'privacy': DrivePrivacy.public, - }; - await bloc.submit(''); - }, - expect: () => [ - DriveCreateInProgress(), - DriveCreateSuccess(), - ], - verify: (_) {}, - ); - - blocTest( - 'create private drive', - build: () => driveCreateCubit, - act: (bloc) async { - bloc.form.value = { - 'name': validDriveName, - 'privacy': DrivePrivacy.private, - }; - await bloc.submit(''); - }, - expect: () => [ - DriveCreateInProgress(), - DriveCreateSuccess(), - ], - verify: (_) {}, - ); - - blocTest( - 'does nothing when submitted without valid form', - build: () => driveCreateCubit, - act: (bloc) => bloc.submit(''), - expect: () => [], - ); - }, skip: 'Needs to update the tests'); + }, + ); }