From 4f8f87f8a87058db1316c67ceeef4486ff77e708 Mon Sep 17 00:00:00 2001 From: NaiJi Date: Sun, 6 Aug 2023 20:28:02 -0300 Subject: [PATCH] feat: Include volume cost to overall monthly cost per server --- assets/translations/en.json | 3 +- assets/translations/ru.json | 3 +- .../digital_ocean/digital_ocean_api.dart | 4 +- .../server_providers/hetzner/hetzner_api.dart | 4 +- .../provider_volume_cubit.dart | 8 +- .../server_installation_cubit.dart | 4 + .../models/launch_installation_data.dart | 3 + .../server_providers/digital_ocean.dart | 35 +- .../providers/server_providers/hetzner.dart | 43 +- .../server_providers/server_provider.dart | 2 +- .../setup/initializing/initializing.dart | 6 +- .../initializing/server_type_picker.dart | 395 ++++++++++-------- 12 files changed, 306 insertions(+), 204 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index f66df742..b68d17ea 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -341,6 +341,7 @@ "choose_server_type_ram": "{} GB of RAM", "choose_server_type_storage": "{} GB of system storage", "choose_server_type_payment_per_month": "{} per month", + "choose_server_type_per_month_description": "{} for server and {} for storage", "no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.", "dns_provider_bad_key_error": "API key is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid", @@ -542,4 +543,4 @@ "reset_onboarding_description": "Reset onboarding switch to show onboarding screen again", "cubit_statuses": "Cubit loading statuses" } -} +} \ No newline at end of file diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 738bceea..33f0c147 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -336,6 +336,7 @@ "choose_server_type_ram": "{} GB у RAM", "choose_server_type_storage": "{} GB системного хранилища", "choose_server_type_payment_per_month": "{} в месяц", + "choose_server_type_per_month_description": "{} за сервер и {} за хранилище", "no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", "dns_provider_bad_key_error": "API ключ неверен", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", @@ -538,4 +539,4 @@ "ignore_tls_description": "Приложение не будет проверять сертификаты TLS при подключении к серверу.", "ignore_tls": "Не проверять сертификаты TLS" } -} +} \ No newline at end of file diff --git a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart index cebb568c..637aabb7 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_api.dart @@ -321,7 +321,7 @@ class DigitalOceanApi extends RestApiMap { ); } - Future> createVolume() async { + Future> createVolume(final int gb) async { DigitalOceanVolume? volume; Response? createVolumeResponse; final Dio client = await getClient(); @@ -331,7 +331,7 @@ class DigitalOceanApi extends RestApiMap { createVolumeResponse = await client.post( '/volumes', data: { - 'size_gigabytes': 10, + 'size_gigabytes': gb, 'name': 'volume${StringGenerators.storageName()}', 'labels': {'labelkey': 'value'}, 'region': region, diff --git a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart index 7fbf3dac..458ac7aa 100644 --- a/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart +++ b/lib/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_api.dart @@ -382,7 +382,7 @@ class HetznerApi extends RestApiMap { ); } - Future> createVolume() async { + Future> createVolume(final int gb) async { Response? createVolumeResponse; HetznerVolume? volume; final Dio client = await getClient(); @@ -390,7 +390,7 @@ class HetznerApi extends RestApiMap { createVolumeResponse = await client.post( '/volumes', data: { - 'size': 10, + 'size': gb, 'name': StringGenerators.storageName(), 'labels': {'labelkey': 'value'}, 'location': region, diff --git a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart index 46137c59..d7166b77 100644 --- a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart +++ b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart @@ -113,9 +113,11 @@ class ApiProviderVolumeCubit return true; } - Future createVolume() async { - final ServerVolume? volume = - (await ProvidersController.currentServerProvider!.createVolume()).data; + Future createVolume(final DiskSize size) async { + final ServerVolume? volume = (await ProvidersController + .currentServerProvider! + .createVolume(size.gibibyte.toInt())) + .data; final diskVolume = DiskVolume(providerVolume: volume); await attachVolume(diskVolume); diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index c669cf75..60a9d0f5 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart'; import 'package:selfprivacy/logic/api_maps/tls_options.dart'; +import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/backups_credential.dart'; import 'package:selfprivacy/logic/models/callback_dialogue_branching.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; @@ -32,6 +33,8 @@ class ServerInstallationCubit extends Cubit { Timer? timer; + final DiskSize initialStorage = DiskSize.fromGibibyte(10); + Future load() async { final ServerInstallationState state = await repository.load(); @@ -257,6 +260,7 @@ class ServerInstallationCubit extends Cubit { serverTypeId: state.serverTypeIdentificator!, errorCallback: clearAppConfig, successCallback: onCreateServerSuccess, + storageSize: initialStorage, ); final result = diff --git a/lib/logic/models/launch_installation_data.dart b/lib/logic/models/launch_installation_data.dart index c1f32ee6..00d89335 100644 --- a/lib/logic/models/launch_installation_data.dart +++ b/lib/logic/models/launch_installation_data.dart @@ -1,3 +1,4 @@ +import 'package:selfprivacy/logic/models/disk_size.dart'; import 'package:selfprivacy/logic/models/hive/server_details.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/user.dart'; @@ -11,6 +12,7 @@ class LaunchInstallationData { required this.serverTypeId, required this.errorCallback, required this.successCallback, + required this.storageSize, }); final User rootUser; @@ -20,4 +22,5 @@ class LaunchInstallationData { final String serverTypeId; final Function() errorCallback; final Function(ServerHostingDetails details) successCallback; + final DiskSize storageSize; } diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index da246b18..62893a0e 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -254,7 +254,9 @@ class DigitalOceanServerProvider extends ServerProvider { try { final int dropletId = serverResult.data!; - final newVolume = (await createVolume()).data; + final newVolume = + (await createVolume(installationData.storageSize.gibibyte.toInt())) + .data; final bool attachedVolume = (await _adapter.api().attachVolume( newVolume!.name, dropletId, @@ -588,10 +590,10 @@ class DigitalOceanServerProvider extends ServerProvider { } @override - Future> createVolume() async { + Future> createVolume(final int gb) async { ServerVolume? volume; - final result = await _adapter.api().createVolume(); + final result = await _adapter.api().createVolume(gb); if (!result.success || result.data == null) { return GenericResult( @@ -708,13 +710,37 @@ class DigitalOceanServerProvider extends ServerProvider { message: result.message, ); } + final resultVolumes = await _adapter.api().getVolumes(); + if (resultVolumes.data.isEmpty || !resultVolumes.success) { + return GenericResult( + success: false, + data: metadata, + code: resultVolumes.code, + message: resultVolumes.message, + ); + } + final resultPricePerGb = await getPricePerGb(); + if (resultPricePerGb.data == null || !resultPricePerGb.success) { + return GenericResult( + success: false, + data: metadata, + code: resultPricePerGb.code, + message: resultPricePerGb.message, + ); + } final List servers = result.data; + final List volumes = resultVolumes.data; + final Price pricePerGb = resultPricePerGb.data!; try { final droplet = servers.firstWhere( (final server) => server['id'] == serverId, ); + final volume = volumes.firstWhere( + (final volume) => droplet['volume_ids'].contains(volume.id), + ); + metadata = [ ServerMetadataEntity( type: MetadataType.id, @@ -739,7 +765,8 @@ class DigitalOceanServerProvider extends ServerProvider { ServerMetadataEntity( type: MetadataType.cost, trId: 'server.monthly_cost', - value: '${droplet['size']['price_monthly']} ${currency.shortcode}', + value: + '${droplet['size']['price_monthly']} + ${(volume.sizeGigabytes * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}', ), ServerMetadataEntity( type: MetadataType.location, diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index ec15801e..aae1e564 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -164,7 +164,9 @@ class HetznerServerProvider extends ServerProvider { Future> launchInstallation( final LaunchInstallationData installationData, ) async { - final volumeResult = await _adapter.api().createVolume(); + final volumeResult = await _adapter.api().createVolume( + installationData.storageSize.gibibyte.toInt(), + ); if (!volumeResult.success || volumeResult.data == null) { return GenericResult( @@ -614,10 +616,10 @@ class HetznerServerProvider extends ServerProvider { } @override - Future> createVolume() async { + Future> createVolume(final int gb) async { ServerVolume? volume; - final result = await _adapter.api().createVolume(); + final result = await _adapter.api().createVolume(gb); if (!result.success || result.data == null) { return GenericResult( @@ -702,22 +704,45 @@ class HetznerServerProvider extends ServerProvider { final int serverId, ) async { List metadata = []; - final result = await _adapter.api().getServers(); - if (result.data.isEmpty || !result.success) { + final resultServers = await _adapter.api().getServers(); + if (resultServers.data.isEmpty || !resultServers.success) { return GenericResult( success: false, data: metadata, - code: result.code, - message: result.message, + code: resultServers.code, + message: resultServers.message, + ); + } + final resultVolumes = await _adapter.api().getVolumes(); + if (resultVolumes.data.isEmpty || !resultVolumes.success) { + return GenericResult( + success: false, + data: metadata, + code: resultVolumes.code, + message: resultVolumes.message, + ); + } + final resultPricePerGb = await getPricePerGb(); + if (resultPricePerGb.data == null || !resultPricePerGb.success) { + return GenericResult( + success: false, + data: metadata, + code: resultPricePerGb.code, + message: resultPricePerGb.message, ); } - final List servers = result.data; + final List servers = resultServers.data; + final List volumes = resultVolumes.data; + final Price pricePerGb = resultPricePerGb.data!; try { final HetznerServerInfo server = servers.firstWhere( (final server) => server.id == serverId, ); + final HetznerVolume volume = volumes + .firstWhere((final volume) => server.volumes.contains(volume.id)); + metadata = [ ServerMetadataEntity( type: MetadataType.id, @@ -743,7 +768,7 @@ class HetznerServerProvider extends ServerProvider { type: MetadataType.cost, trId: 'server.monthly_cost', value: - '${server.serverType.prices[1].monthly.toStringAsFixed(2)} ${currency.shortcode}', + '${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}', ), ServerMetadataEntity( type: MetadataType.location, diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index cfcf3f5e..2a1a8b08 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -101,7 +101,7 @@ abstract class ServerProvider { /// Tries to create an empty unattached [ServerVolume]. /// /// If success, returns this volume information. - Future> createVolume(); + Future> createVolume(final int gb); /// Tries to delete the requested accessible [ServerVolume]. Future> deleteVolume(final ServerVolume volume); diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index c74fa4a4..fb01df9c 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -3,6 +3,7 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; @@ -31,6 +32,7 @@ class InitializingPage extends StatelessWidget { @override Widget build(final BuildContext context) { final cubit = context.watch(); + final volumeCubit = context.read(); if (cubit.state is ServerInstallationRecovery) { return const RecoveryRouting(); @@ -39,7 +41,7 @@ class InitializingPage extends StatelessWidget { if (cubit.state is! ServerInstallationFinished) { actualInitializingPage = [ () => _stepServerProviderToken(cubit), - () => _stepServerType(cubit), + () => _stepServerType(cubit, volumeCubit), () => _stepDnsProviderToken(cubit), () => _stepBackblaze(cubit), () => _stepDomain(cubit), @@ -226,6 +228,7 @@ class InitializingPage extends StatelessWidget { Widget _stepServerType( final ServerInstallationCubit serverInstallationCubit, + final ApiProviderVolumeCubit apiProviderVolumeCubit, ) => BlocProvider( create: (final context) => @@ -233,6 +236,7 @@ class InitializingPage extends StatelessWidget { child: Builder( builder: (final context) => ServerTypePicker( serverInstallationCubit: serverInstallationCubit, + apiProviderVolumeCubit: apiProviderVolumeCubit, ), ), ); diff --git a/lib/ui/pages/setup/initializing/server_type_picker.dart b/lib/ui/pages/setup/initializing/server_type_picker.dart index 6212bec3..950b0f4d 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/illustrations/stray_deer.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart'; +import 'package:selfprivacy/logic/models/price.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; @@ -12,10 +14,12 @@ import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; class ServerTypePicker extends StatefulWidget { const ServerTypePicker({ required this.serverInstallationCubit, + required this.apiProviderVolumeCubit, super.key, }); final ServerInstallationCubit serverInstallationCubit; + final ApiProviderVolumeCubit apiProviderVolumeCubit; @override State createState() => _ServerTypePickerState(); @@ -43,6 +47,7 @@ class _ServerTypePickerState extends State { return SelectTypePage( location: serverProviderLocation!, serverInstallationCubit: widget.serverInstallationCubit, + apiProviderVolumeCubit: widget.apiProviderVolumeCubit, backToLocationPickingCallback: () { setServerProviderLocation(null); }, @@ -145,202 +150,232 @@ class SelectTypePage extends StatelessWidget { required this.backToLocationPickingCallback, required this.location, required this.serverInstallationCubit, + required this.apiProviderVolumeCubit, super.key, }); final ServerProviderLocation location; final ServerInstallationCubit serverInstallationCubit; + final ApiProviderVolumeCubit apiProviderVolumeCubit; final Function backToLocationPickingCallback; @override - Widget build(final BuildContext context) => FutureBuilder( - future: serverInstallationCubit.fetchAvailableTypesByLocation(location), - builder: ( - final BuildContext context, - final AsyncSnapshot snapshot, - ) { - if (snapshot.hasData) { - if ((snapshot.data as List).isEmpty) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.locations_not_found'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.locations_not_found_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - LayoutBuilder( - builder: (final context, final constraints) => CustomPaint( - size: Size( - constraints.maxWidth, - (constraints.maxWidth * 1).toDouble(), - ), - painter: StrayDeerPainter( - colorScheme: Theme.of(context).colorScheme, - colorPalette: context - .read() - .state - .corePaletteOrDefault, - ), + Widget build(final BuildContext context) { + final Future> serverTypes = + serverInstallationCubit.fetchAvailableTypesByLocation(location); + final Future pricePerGb = apiProviderVolumeCubit.getPricePerGb(); + return FutureBuilder( + future: Future.wait([ + serverTypes, + pricePerGb, + ]), + builder: ( + final BuildContext context, + final AsyncSnapshot> snapshot, + ) { + if (snapshot.hasData) { + if ((snapshot.data![0] as List).isEmpty || + (snapshot.data![1] as Price?) == null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.locations_not_found'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.locations_not_found_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + LayoutBuilder( + builder: (final context, final constraints) => CustomPaint( + size: Size( + constraints.maxWidth, + (constraints.maxWidth * 1).toDouble(), + ), + painter: StrayDeerPainter( + colorScheme: Theme.of(context).colorScheme, + colorPalette: context + .read() + .state + .corePaletteOrDefault, ), ), - const SizedBox(height: 16), - BrandButton.rised( - onPressed: () { - backToLocationPickingCallback(); - }, - text: 'initializing.back_to_locations'.tr(), - ), - ], - ); - } - return ResponsiveLayoutWithInfobox( - topChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'initializing.choose_server_type'.tr(), - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.choose_server_type_text'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - primaryColumn: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ...(snapshot.data! as List).map( - (final type) => Column( - children: [ - SizedBox( - width: double.infinity, - child: InkWell( - onTap: () { - serverInstallationCubit.setServerType(type); - }, - child: Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - type.title, - style: Theme.of(context) - .textTheme - .titleMedium, - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.memory_outlined, - color: Theme.of(context) - .colorScheme - .onSurface, + ), + const SizedBox(height: 16), + BrandButton.rised( + onPressed: () { + backToLocationPickingCallback(); + }, + text: 'initializing.back_to_locations'.tr(), + ), + ], + ); + } + return ResponsiveLayoutWithInfobox( + topChild: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.choose_server_type'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.choose_server_type_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + primaryColumn: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...(snapshot.data![0] as List).map( + (final type) => Column( + children: [ + SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + serverInstallationCubit.setServerType(type); + }, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + type.title, + style: + Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.memory_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'server.core_count'.plural(type.cores), + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.memory_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_ram' + .tr(args: [type.ram.toString()]), + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.sd_card_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_storage' + .tr( + args: [type.disk.gibibyte.toString()], ), - const SizedBox(width: 8), - Text( - 'server.core_count' - .plural(type.cores), - style: Theme.of(context) - .textTheme - .bodyMedium, + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ], + ), + const SizedBox(height: 8), + const Divider(height: 8), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.payments_outlined, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_payment_per_month' + .tr( + args: [ + '${type.price.value + (serverInstallationCubit.initialStorage.gibibyte * (snapshot.data![1] as Price).value)} ${type.price.currency.shortcode}' + ], ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.memory_outlined, - color: Theme.of(context) - .colorScheme - .onSurface, + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ], + ), + Row( + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context) + .colorScheme + .onSurface, + ), + const SizedBox(width: 8), + Text( + 'initializing.choose_server_type_per_month_description' + .tr( + args: [ + type.price.value.toString(), + '${serverInstallationCubit.initialStorage.gibibyte * (snapshot.data![1] as Price).value}', + ], ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_ram' - .tr(args: [type.ram.toString()]), - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.sd_card_outlined, - color: Theme.of(context) - .colorScheme - .onSurface, - ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_storage' - .tr( - args: [ - type.disk.gibibyte.toString() - ], - ), - style: Theme.of(context) - .textTheme - .bodyMedium, - ), - ], - ), - const SizedBox(height: 8), - const Divider(height: 8), - const SizedBox(height: 8), - Row( - children: [ - Icon( - Icons.payments_outlined, - color: Theme.of(context) - .colorScheme - .onSurface, - ), - const SizedBox(width: 8), - Text( - 'initializing.choose_server_type_payment_per_month' - .tr( - args: [ - '${type.price.value.toString()} ${type.price.currency.shortcode}' - ], - ), - style: Theme.of(context) - .textTheme - .bodyLarge, - ), - ], - ), - ], - ), + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ], + ), + ], ), ), ), ), - const SizedBox(height: 8), - ], - ), + ), + const SizedBox(height: 8), + ], ), - ], - ), - secondaryColumn: - InfoBox(text: 'initializing.choose_server_type_notice'.tr()), - ); - } else { - return const Center(child: CircularProgressIndicator()); - } - }, - ); + ), + ], + ), + secondaryColumn: + InfoBox(text: 'initializing.choose_server_type_notice'.tr()), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ); + } }