fix: Include IPv4 cost to overall server cost

pull/270/head
NaiJi ✨ 2023-08-07 07:51:47 -03:00
parent 4f8f87f8a8
commit 037498070a
13 changed files with 165 additions and 72 deletions

View File

@ -125,6 +125,7 @@
"monthly_cost": "Monthly cost", "monthly_cost": "Monthly cost",
"location": "Location", "location": "Location",
"provider": "Provider", "provider": "Provider",
"pricing_error": "Couldn't fetch provider prices",
"core_count": { "core_count": {
"one": "{} core", "one": "{} core",
"two": "{} cores", "two": "{} cores",
@ -341,7 +342,9 @@
"choose_server_type_ram": "{} GB of RAM", "choose_server_type_ram": "{} GB of RAM",
"choose_server_type_storage": "{} GB of system storage", "choose_server_type_storage": "{} GB of system storage",
"choose_server_type_payment_per_month": "{} per month", "choose_server_type_payment_per_month": "{} per month",
"choose_server_type_per_month_description": "{} for server and {} for storage", "choose_server_type_payment_server": "{} for server",
"choose_server_type_payment_storage": "{} for additional storage",
"choose_server_type_payment_ip": "{} for public IPv4",
"no_server_types_found": "No available server types found. Make sure your account is accessible and try to change your server location.", "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", "dns_provider_bad_key_error": "API key is invalid",
"backblaze_bad_key_error": "Backblaze storage information is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid",

View File

@ -125,6 +125,7 @@
"monthly_cost": "Ежемесячная стоимость", "monthly_cost": "Ежемесячная стоимость",
"location": "Размещение", "location": "Размещение",
"provider": "Провайдер", "provider": "Провайдер",
"pricing_error": "Не удалось получить цены провайдера",
"core_count": { "core_count": {
"one": "{} ядро", "one": "{} ядро",
"two": "{} ядра", "two": "{} ядра",
@ -336,7 +337,9 @@
"choose_server_type_ram": "{} GB у RAM", "choose_server_type_ram": "{} GB у RAM",
"choose_server_type_storage": "{} GB системного хранилища", "choose_server_type_storage": "{} GB системного хранилища",
"choose_server_type_payment_per_month": "{} в месяц", "choose_server_type_payment_per_month": "{} в месяц",
"choose_server_type_per_month_description": "{} за сервер и {} за хранилище", "choose_server_type_payment_server": "{} за сам сервер",
"choose_server_type_payment_storage": "{} за расширяемое хранилище",
"choose_server_type_payment_ip": "{} за публичный IPv4",
"no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", "no_server_types_found": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...",
"dns_provider_bad_key_error": "API ключ неверен", "dns_provider_bad_key_error": "API ключ неверен",
"backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна",

View File

@ -321,8 +321,8 @@ class HetznerApi extends RestApiMap {
return GenericResult(success: true, data: null); return GenericResult(success: true, data: null);
} }
Future<GenericResult<double?>> getPricePerGb() async { Future<GenericResult<HetznerPricing?>> getPricing() async {
double? price; HetznerPricing? pricing;
final Response pricingResponse; final Response pricingResponse;
final Dio client = await getClient(); final Dio client = await getClient();
@ -331,19 +331,34 @@ class HetznerApi extends RestApiMap {
final volume = pricingResponse.data['pricing']['volume']; final volume = pricingResponse.data['pricing']['volume'];
final volumePrice = volume['price_per_gb_month']['gross']; final volumePrice = volume['price_per_gb_month']['gross'];
price = double.parse(volumePrice); final primaryIps = pricingResponse.data['pricing']['primary_ips'];
String? ipPrice;
for (final primaryIp in primaryIps) {
if (primaryIp['type'] == 'ipv4') {
for (final primaryIpPrice in primaryIp['prices']) {
if (primaryIpPrice['location'] == region!) {
ipPrice = primaryIpPrice['price_monthly']['gross'];
}
}
}
}
pricing = HetznerPricing(
region!,
double.parse(volumePrice),
double.parse(ipPrice!),
);
} catch (e) { } catch (e) {
print(e); print(e);
return GenericResult( return GenericResult(
success: false, success: false,
data: price, data: pricing,
message: e.toString(), message: e.toString(),
); );
} finally { } finally {
client.close(); client.close();
} }
return GenericResult(success: true, data: price); return GenericResult(success: true, data: pricing);
} }
Future<GenericResult<List<HetznerVolume>>> getVolumes({ Future<GenericResult<List<HetznerVolume>>> getVolumes({

View File

@ -26,8 +26,17 @@ class ApiProviderVolumeCubit
} }
} }
Future<Price?> getPricePerGb() async => Future<Price?> getPricePerGb() async {
(await ProvidersController.currentServerProvider!.getPricePerGb()).data; Price? price;
final pricingResult =
await ProvidersController.currentServerProvider!.getAdditionalPricing();
if (pricingResult.data == null || !pricingResult.success) {
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
return price;
}
price = pricingResult.data!.perVolumeGb;
return price;
}
Future<void> refresh() async { Future<void> refresh() async {
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));

View File

@ -14,6 +14,7 @@ import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/launch_installation_data.dart'; import 'package:selfprivacy/logic/models/launch_installation_data.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart'; import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/models/server_basic_info.dart'; import 'package:selfprivacy/logic/models/server_basic_info.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
@ -150,6 +151,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
return apiResult.data; return apiResult.data;
} }
Future<AdditionalPricing?> fetchAvailableAdditionalPricing() async {
AdditionalPricing? prices;
final pricingResult =
await ProvidersController.currentServerProvider!.getAdditionalPricing();
if (pricingResult.data == null || !pricingResult.success) {
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
return prices;
}
prices = pricingResult.data;
return prices;
}
void setServerProviderKey(final String serverProviderKey) async { void setServerProviderKey(final String serverProviderKey) async {
await repository.saveServerProviderKey(serverProviderKey); await repository.saveServerProviderKey(serverProviderKey);
@ -170,12 +184,14 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
); );
} }
Future<void> setLocationIdentifier(final String locationId) async {
await ProvidersController.currentServerProvider!
.trySetServerLocation(locationId);
}
void setServerType(final ServerType serverType) async { void setServerType(final ServerType serverType) async {
await repository.saveServerType(serverType); await repository.saveServerType(serverType);
await ProvidersController.currentServerProvider!
.trySetServerLocation(serverType.location.identifier);
emit( emit(
(state as ServerInstallationNotFinished).copyWith( (state as ServerInstallationNotFinished).copyWith(
serverTypeIdentificator: serverType.identifier, serverTypeIdentificator: serverType.identifier,

View File

@ -190,3 +190,22 @@ class HetznerVolume {
static HetznerVolume fromJson(final Map<String, dynamic> json) => static HetznerVolume fromJson(final Map<String, dynamic> json) =>
_$HetznerVolumeFromJson(json); _$HetznerVolumeFromJson(json);
} }
/// Prices for Hetzner resources in Euro (monthly).
/// https://docs.hetzner.cloud/#pricing
class HetznerPricing {
HetznerPricing(
this.region,
this.perVolumeGb,
this.perPublicIpv4,
);
/// Region name to which current price listing applies
final String region;
/// The cost of Volume per GB/month
final double perVolumeGb;
/// Costs of Primary IP type
final double perPublicIpv4;
}

View File

@ -53,3 +53,12 @@ class Currency {
final String? fontcode; final String? fontcode;
final String? symbol; final String? symbol;
} }
class AdditionalPricing {
AdditionalPricing({
required this.perVolumeGb,
required this.perPublicIpv4,
});
final Price perVolumeGb;
final Price perPublicIpv4;
}

View File

@ -529,14 +529,20 @@ class DigitalOceanServerProvider extends ServerProvider {
); );
} }
/// Hardcoded on their documentation and there is no pricing API at all
/// Probably we should scrap the doc page manually
@override @override
Future<GenericResult<Price?>> getPricePerGb() async => GenericResult( Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async =>
GenericResult(
success: true, success: true,
data: Price( data: AdditionalPricing(
value: 0.10, perVolumeGb: Price(
currency: currency, /// Hardcoded in their documentation and there is no pricing API
value: 0.10,
currency: currency,
),
perPublicIpv4: Price(
value: 0,
currency: currency,
),
), ),
); );
@ -719,7 +725,7 @@ class DigitalOceanServerProvider extends ServerProvider {
message: resultVolumes.message, message: resultVolumes.message,
); );
} }
final resultPricePerGb = await getPricePerGb(); final resultPricePerGb = await getAdditionalPricing();
if (resultPricePerGb.data == null || !resultPricePerGb.success) { if (resultPricePerGb.data == null || !resultPricePerGb.success) {
return GenericResult( return GenericResult(
success: false, success: false,
@ -731,8 +737,8 @@ class DigitalOceanServerProvider extends ServerProvider {
final List servers = result.data; final List servers = result.data;
final List<DigitalOceanVolume> volumes = resultVolumes.data; final List<DigitalOceanVolume> volumes = resultVolumes.data;
final Price pricePerGb = resultPricePerGb.data!;
try { try {
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
final droplet = servers.firstWhere( final droplet = servers.firstWhere(
(final server) => server['id'] == serverId, (final server) => server['id'] == serverId,
); );

View File

@ -545,8 +545,8 @@ class HetznerServerProvider extends ServerProvider {
} }
@override @override
Future<GenericResult<Price?>> getPricePerGb() async { Future<GenericResult<AdditionalPricing?>> getAdditionalPricing() async {
final result = await _adapter.api().getPricePerGb(); final result = await _adapter.api().getPricing();
if (!result.success || result.data == null) { if (!result.success || result.data == null) {
return GenericResult( return GenericResult(
@ -559,9 +559,15 @@ class HetznerServerProvider extends ServerProvider {
return GenericResult( return GenericResult(
success: true, success: true,
data: Price( data: AdditionalPricing(
value: result.data!, perVolumeGb: Price(
currency: currency, value: result.data!.perVolumeGb,
currency: currency,
),
perPublicIpv4: Price(
value: result.data!.perPublicIpv4,
currency: currency,
),
), ),
); );
} }
@ -722,7 +728,7 @@ class HetznerServerProvider extends ServerProvider {
message: resultVolumes.message, message: resultVolumes.message,
); );
} }
final resultPricePerGb = await getPricePerGb(); final resultPricePerGb = await getAdditionalPricing();
if (resultPricePerGb.data == null || !resultPricePerGb.success) { if (resultPricePerGb.data == null || !resultPricePerGb.success) {
return GenericResult( return GenericResult(
success: false, success: false,
@ -734,14 +740,16 @@ class HetznerServerProvider extends ServerProvider {
final List<HetznerServerInfo> servers = resultServers.data; final List<HetznerServerInfo> servers = resultServers.data;
final List<HetznerVolume> volumes = resultVolumes.data; final List<HetznerVolume> volumes = resultVolumes.data;
final Price pricePerGb = resultPricePerGb.data!;
try { try {
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
final Price pricePerIp = resultPricePerGb.data!.perPublicIpv4;
final HetznerServerInfo server = servers.firstWhere( final HetznerServerInfo server = servers.firstWhere(
(final server) => server.id == serverId, (final server) => server.id == serverId,
); );
final HetznerVolume volume = volumes.firstWhere(
final HetznerVolume volume = volumes (final volume) => server.volumes.contains(volume.id),
.firstWhere((final volume) => server.volumes.contains(volume.id)); );
metadata = [ metadata = [
ServerMetadataEntity( ServerMetadataEntity(
@ -768,7 +776,8 @@ class HetznerServerProvider extends ServerProvider {
type: MetadataType.cost, type: MetadataType.cost,
trId: 'server.monthly_cost', trId: 'server.monthly_cost',
value: value:
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} ${currency.shortcode}', // TODO: Make more descriptive
'${server.serverType.prices[1].monthly.toStringAsFixed(2)} + ${(volume.size * pricePerGb.value).toStringAsFixed(2)} + ${pricePerIp.value.toStringAsFixed(2)} ${currency.shortcode}',
), ),
ServerMetadataEntity( ServerMetadataEntity(
type: MetadataType.location, type: MetadataType.location,

View File

@ -90,9 +90,9 @@ abstract class ServerProvider {
/// answered the request. /// answered the request.
Future<GenericResult<DateTime?>> restart(final int serverId); Future<GenericResult<DateTime?>> restart(final int serverId);
/// Returns [Price] information per one gigabyte of storage extension for /// Returns [Price] information map of all additional resources, excluding
/// the requested accessible machine. /// main server type pricing
Future<GenericResult<Price?>> getPricePerGb(); Future<GenericResult<AdditionalPricing?>> getAdditionalPricing();
/// Returns [ServerVolume] of all available volumes /// Returns [ServerVolume] of all available volumes
/// assigned to the authorized user and attached to active machine. /// assigned to the authorized user and attached to active machine.

View File

@ -45,6 +45,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
late double _currentSliderGbValue; late double _currentSliderGbValue;
double _pricePerGb = 1.0; double _pricePerGb = 1.0;
// TODO: Wtfff hardcode?!?!?
final DiskSize maxSize = const DiskSize(byte: 500000000000); final DiskSize maxSize = const DiskSize(byte: 500000000000);
late DiskSize minSize; late DiskSize minSize;

View File

@ -3,7 +3,6 @@ import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/server_provider_form_cubit.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/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart';
import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart';
@ -32,7 +31,6 @@ class InitializingPage extends StatelessWidget {
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final cubit = context.watch<ServerInstallationCubit>(); final cubit = context.watch<ServerInstallationCubit>();
final volumeCubit = context.read<ApiProviderVolumeCubit>();
if (cubit.state is ServerInstallationRecovery) { if (cubit.state is ServerInstallationRecovery) {
return const RecoveryRouting(); return const RecoveryRouting();
@ -41,7 +39,7 @@ class InitializingPage extends StatelessWidget {
if (cubit.state is! ServerInstallationFinished) { if (cubit.state is! ServerInstallationFinished) {
actualInitializingPage = [ actualInitializingPage = [
() => _stepServerProviderToken(cubit), () => _stepServerProviderToken(cubit),
() => _stepServerType(cubit, volumeCubit), () => _stepServerType(cubit),
() => _stepDnsProviderToken(cubit), () => _stepDnsProviderToken(cubit),
() => _stepBackblaze(cubit), () => _stepBackblaze(cubit),
() => _stepDomain(cubit), () => _stepDomain(cubit),
@ -228,7 +226,6 @@ class InitializingPage extends StatelessWidget {
Widget _stepServerType( Widget _stepServerType(
final ServerInstallationCubit serverInstallationCubit, final ServerInstallationCubit serverInstallationCubit,
final ApiProviderVolumeCubit apiProviderVolumeCubit,
) => ) =>
BlocProvider( BlocProvider(
create: (final context) => create: (final context) =>
@ -236,7 +233,6 @@ class InitializingPage extends StatelessWidget {
child: Builder( child: Builder(
builder: (final context) => ServerTypePicker( builder: (final context) => ServerTypePicker(
serverInstallationCubit: serverInstallationCubit, serverInstallationCubit: serverInstallationCubit,
apiProviderVolumeCubit: apiProviderVolumeCubit,
), ),
), ),
); );

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/illustrations/stray_deer.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_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_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/price.dart';
import 'package:selfprivacy/logic/models/server_provider_location.dart'; import 'package:selfprivacy/logic/models/server_provider_location.dart';
import 'package:selfprivacy/logic/models/server_type.dart'; import 'package:selfprivacy/logic/models/server_type.dart';
@ -14,12 +13,10 @@ import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart';
class ServerTypePicker extends StatefulWidget { class ServerTypePicker extends StatefulWidget {
const ServerTypePicker({ const ServerTypePicker({
required this.serverInstallationCubit, required this.serverInstallationCubit,
required this.apiProviderVolumeCubit,
super.key, super.key,
}); });
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
final ApiProviderVolumeCubit apiProviderVolumeCubit;
@override @override
State<ServerTypePicker> createState() => _ServerTypePickerState(); State<ServerTypePicker> createState() => _ServerTypePickerState();
@ -29,7 +26,12 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
ServerProviderLocation? serverProviderLocation; ServerProviderLocation? serverProviderLocation;
ServerType? serverType; ServerType? serverType;
void setServerProviderLocation(final ServerProviderLocation? location) { void setServerProviderLocation(final ServerProviderLocation? location) async {
if (location != null) {
await widget.serverInstallationCubit.setLocationIdentifier(
location.identifier,
);
}
setState(() { setState(() {
serverProviderLocation = location; serverProviderLocation = location;
}); });
@ -47,7 +49,6 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
return SelectTypePage( return SelectTypePage(
location: serverProviderLocation!, location: serverProviderLocation!,
serverInstallationCubit: widget.serverInstallationCubit, serverInstallationCubit: widget.serverInstallationCubit,
apiProviderVolumeCubit: widget.apiProviderVolumeCubit,
backToLocationPickingCallback: () { backToLocationPickingCallback: () {
setServerProviderLocation(null); setServerProviderLocation(null);
}, },
@ -150,24 +151,23 @@ class SelectTypePage extends StatelessWidget {
required this.backToLocationPickingCallback, required this.backToLocationPickingCallback,
required this.location, required this.location,
required this.serverInstallationCubit, required this.serverInstallationCubit,
required this.apiProviderVolumeCubit,
super.key, super.key,
}); });
final ServerProviderLocation location; final ServerProviderLocation location;
final ServerInstallationCubit serverInstallationCubit; final ServerInstallationCubit serverInstallationCubit;
final ApiProviderVolumeCubit apiProviderVolumeCubit;
final Function backToLocationPickingCallback; final Function backToLocationPickingCallback;
@override @override
Widget build(final BuildContext context) { Widget build(final BuildContext context) {
final Future<List<ServerType>> serverTypes = final Future<List<ServerType>> serverTypes =
serverInstallationCubit.fetchAvailableTypesByLocation(location); serverInstallationCubit.fetchAvailableTypesByLocation(location);
final Future<Price?> pricePerGb = apiProviderVolumeCubit.getPricePerGb(); final Future<AdditionalPricing?> prices =
serverInstallationCubit.fetchAvailableAdditionalPricing();
return FutureBuilder( return FutureBuilder(
future: Future.wait([ future: Future.wait([
serverTypes, serverTypes,
pricePerGb, prices,
]), ]),
builder: ( builder: (
final BuildContext context, final BuildContext context,
@ -175,7 +175,7 @@ class SelectTypePage extends StatelessWidget {
) { ) {
if (snapshot.hasData) { if (snapshot.hasData) {
if ((snapshot.data![0] as List<ServerType>).isEmpty || if ((snapshot.data![0] as List<ServerType>).isEmpty ||
(snapshot.data![1] as Price?) == null) { (snapshot.data![1] as AdditionalPricing?) == null) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -213,6 +213,10 @@ class SelectTypePage extends StatelessWidget {
], ],
); );
} }
final prices = snapshot.data![1] as AdditionalPricing;
final storagePrice = serverInstallationCubit.initialStorage.gibibyte *
prices.perVolumeGb.value;
final publicIpPrice = prices.perPublicIpv4.value;
return ResponsiveLayoutWithInfobox( return ResponsiveLayoutWithInfobox(
topChild: Column( topChild: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -325,7 +329,7 @@ class SelectTypePage extends StatelessWidget {
'initializing.choose_server_type_payment_per_month' 'initializing.choose_server_type_payment_per_month'
.tr( .tr(
args: [ args: [
'${type.price.value + (serverInstallationCubit.initialStorage.gibibyte * (snapshot.data![1] as Price).value)} ${type.price.currency.shortcode}' '${(type.price.value + storagePrice + publicIpPrice).toStringAsFixed(4)} ${type.price.currency.shortcode}'
], ],
), ),
style: Theme.of(context) style: Theme.of(context)
@ -334,29 +338,32 @@ class SelectTypePage extends StatelessWidget {
), ),
], ],
), ),
Row( Text(
children: [ 'initializing.choose_server_type_payment_server'
Icon( .tr(
Icons.info_outline, args: [type.price.value.toString()],
color: Theme.of(context) ),
.colorScheme style:
.onSurface, Theme.of(context).textTheme.bodyMedium,
),
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}',
],
),
style: Theme.of(context)
.textTheme
.bodyLarge,
),
],
), ),
Text(
'initializing.choose_server_type_payment_storage'
.tr(
args: [storagePrice.toString()],
),
style:
Theme.of(context).textTheme.bodyMedium,
),
if (publicIpPrice != 0)
Text(
'initializing.choose_server_type_payment_ip'
.tr(
args: [publicIpPrice.toString()],
),
style: Theme.of(context)
.textTheme
.bodyMedium,
),
], ],
), ),
), ),