diff --git a/assets/translations/en.json b/assets/translations/en.json index b68d17ea..a8669cb1 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -125,6 +125,7 @@ "monthly_cost": "Monthly cost", "location": "Location", "provider": "Provider", + "pricing_error": "Couldn't fetch provider prices", "core_count": { "one": "{} core", "two": "{} cores", @@ -341,7 +342,9 @@ "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", + "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.", "dns_provider_bad_key_error": "API key is invalid", "backblaze_bad_key_error": "Backblaze storage information is invalid", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 33f0c147..81ba4a61 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -125,6 +125,7 @@ "monthly_cost": "Ежемесячная стоимость", "location": "Размещение", "provider": "Провайдер", + "pricing_error": "Не удалось получить цены провайдера", "core_count": { "one": "{} ядро", "two": "{} ядра", @@ -336,7 +337,9 @@ "choose_server_type_ram": "{} GB у RAM", "choose_server_type_storage": "{} GB системного хранилища", "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": "Не найдено доступных типов сервера! Пожалуйста, убедитесь, что у вас есть доступ к провайдеру сервера...", "dns_provider_bad_key_error": "API ключ неверен", "backblaze_bad_key_error": "Информация о Backblaze хранилище неверна", 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 458ac7aa..2631550e 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 @@ -321,8 +321,8 @@ class HetznerApi extends RestApiMap { return GenericResult(success: true, data: null); } - Future> getPricePerGb() async { - double? price; + Future> getPricing() async { + HetznerPricing? pricing; final Response pricingResponse; final Dio client = await getClient(); @@ -331,19 +331,34 @@ class HetznerApi extends RestApiMap { final volume = pricingResponse.data['pricing']['volume']; 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) { print(e); return GenericResult( success: false, - data: price, + data: pricing, message: e.toString(), ); } finally { client.close(); } - return GenericResult(success: true, data: price); + return GenericResult(success: true, data: pricing); } Future>> getVolumes({ diff --git a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart index d7166b77..fed7b083 100644 --- a/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart +++ b/lib/logic/cubit/provider_volumes/provider_volume_cubit.dart @@ -26,8 +26,17 @@ class ApiProviderVolumeCubit } } - Future getPricePerGb() async => - (await ProvidersController.currentServerProvider!.getPricePerGb()).data; + Future getPricePerGb() async { + Price? price; + final pricingResult = + await ProvidersController.currentServerProvider!.getAdditionalPricing(); + if (pricingResult.data == null || !pricingResult.success) { + getIt().showSnackBar('server.pricing_error'.tr()); + return price; + } + price = pricingResult.data!.perVolumeGb; + return price; + } Future refresh() async { emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false)); diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 60a9d0f5..fe4449e7 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -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/hive/server_domain.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_provider_location.dart'; import 'package:selfprivacy/logic/models/server_type.dart'; @@ -150,6 +151,19 @@ class ServerInstallationCubit extends Cubit { return apiResult.data; } + Future fetchAvailableAdditionalPricing() async { + AdditionalPricing? prices; + final pricingResult = + await ProvidersController.currentServerProvider!.getAdditionalPricing(); + if (pricingResult.data == null || !pricingResult.success) { + getIt().showSnackBar('server.pricing_error'.tr()); + return prices; + } + + prices = pricingResult.data; + return prices; + } + void setServerProviderKey(final String serverProviderKey) async { await repository.saveServerProviderKey(serverProviderKey); @@ -170,12 +184,14 @@ class ServerInstallationCubit extends Cubit { ); } + Future setLocationIdentifier(final String locationId) async { + await ProvidersController.currentServerProvider! + .trySetServerLocation(locationId); + } + void setServerType(final ServerType serverType) async { await repository.saveServerType(serverType); - await ProvidersController.currentServerProvider! - .trySetServerLocation(serverType.location.identifier); - emit( (state as ServerInstallationNotFinished).copyWith( serverTypeIdentificator: serverType.identifier, diff --git a/lib/logic/models/json/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart index ece29de0..6d51f2b8 100644 --- a/lib/logic/models/json/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -190,3 +190,22 @@ class HetznerVolume { static HetznerVolume fromJson(final Map 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; +} diff --git a/lib/logic/models/price.dart b/lib/logic/models/price.dart index b60cf5bf..494b1511 100644 --- a/lib/logic/models/price.dart +++ b/lib/logic/models/price.dart @@ -53,3 +53,12 @@ class Currency { final String? fontcode; final String? symbol; } + +class AdditionalPricing { + AdditionalPricing({ + required this.perVolumeGb, + required this.perPublicIpv4, + }); + final Price perVolumeGb; + final Price perPublicIpv4; +} diff --git a/lib/logic/providers/server_providers/digital_ocean.dart b/lib/logic/providers/server_providers/digital_ocean.dart index 62893a0e..db770b42 100644 --- a/lib/logic/providers/server_providers/digital_ocean.dart +++ b/lib/logic/providers/server_providers/digital_ocean.dart @@ -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 - Future> getPricePerGb() async => GenericResult( + Future> getAdditionalPricing() async => + GenericResult( success: true, - data: Price( - value: 0.10, - currency: currency, + data: AdditionalPricing( + perVolumeGb: Price( + /// 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, ); } - final resultPricePerGb = await getPricePerGb(); + final resultPricePerGb = await getAdditionalPricing(); if (resultPricePerGb.data == null || !resultPricePerGb.success) { return GenericResult( success: false, @@ -731,8 +737,8 @@ class DigitalOceanServerProvider extends ServerProvider { final List servers = result.data; final List volumes = resultVolumes.data; - final Price pricePerGb = resultPricePerGb.data!; try { + final Price pricePerGb = resultPricePerGb.data!.perVolumeGb; final droplet = servers.firstWhere( (final server) => server['id'] == serverId, ); diff --git a/lib/logic/providers/server_providers/hetzner.dart b/lib/logic/providers/server_providers/hetzner.dart index aae1e564..aef446a3 100644 --- a/lib/logic/providers/server_providers/hetzner.dart +++ b/lib/logic/providers/server_providers/hetzner.dart @@ -545,8 +545,8 @@ class HetznerServerProvider extends ServerProvider { } @override - Future> getPricePerGb() async { - final result = await _adapter.api().getPricePerGb(); + Future> getAdditionalPricing() async { + final result = await _adapter.api().getPricing(); if (!result.success || result.data == null) { return GenericResult( @@ -559,9 +559,15 @@ class HetznerServerProvider extends ServerProvider { return GenericResult( success: true, - data: Price( - value: result.data!, - currency: currency, + data: AdditionalPricing( + perVolumeGb: Price( + 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, ); } - final resultPricePerGb = await getPricePerGb(); + final resultPricePerGb = await getAdditionalPricing(); if (resultPricePerGb.data == null || !resultPricePerGb.success) { return GenericResult( success: false, @@ -734,14 +740,16 @@ class HetznerServerProvider extends ServerProvider { final List servers = resultServers.data; final List volumes = resultVolumes.data; - final Price pricePerGb = resultPricePerGb.data!; + try { + final Price pricePerGb = resultPricePerGb.data!.perVolumeGb; + final Price pricePerIp = resultPricePerGb.data!.perPublicIpv4; final HetznerServerInfo server = servers.firstWhere( (final server) => server.id == serverId, ); - - final HetznerVolume volume = volumes - .firstWhere((final volume) => server.volumes.contains(volume.id)); + final HetznerVolume volume = volumes.firstWhere( + (final volume) => server.volumes.contains(volume.id), + ); metadata = [ ServerMetadataEntity( @@ -768,7 +776,8 @@ class HetznerServerProvider extends ServerProvider { type: MetadataType.cost, trId: 'server.monthly_cost', 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( type: MetadataType.location, diff --git a/lib/logic/providers/server_providers/server_provider.dart b/lib/logic/providers/server_providers/server_provider.dart index 2a1a8b08..ce7b6944 100644 --- a/lib/logic/providers/server_providers/server_provider.dart +++ b/lib/logic/providers/server_providers/server_provider.dart @@ -90,9 +90,9 @@ abstract class ServerProvider { /// answered the request. Future> restart(final int serverId); - /// Returns [Price] information per one gigabyte of storage extension for - /// the requested accessible machine. - Future> getPricePerGb(); + /// Returns [Price] information map of all additional resources, excluding + /// main server type pricing + Future> getAdditionalPricing(); /// Returns [ServerVolume] of all available volumes /// assigned to the authorized user and attached to active machine. diff --git a/lib/ui/pages/server_storage/extending_volume.dart b/lib/ui/pages/server_storage/extending_volume.dart index 39872494..752a6281 100644 --- a/lib/ui/pages/server_storage/extending_volume.dart +++ b/lib/ui/pages/server_storage/extending_volume.dart @@ -45,6 +45,7 @@ class _ExtendingVolumePageState extends State { late double _currentSliderGbValue; double _pricePerGb = 1.0; + // TODO: Wtfff hardcode?!?!? final DiskSize maxSize = const DiskSize(byte: 500000000000); late DiskSize minSize; diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index fb01df9c..c74fa4a4 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -3,7 +3,6 @@ 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'; @@ -32,7 +31,6 @@ 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(); @@ -41,7 +39,7 @@ class InitializingPage extends StatelessWidget { if (cubit.state is! ServerInstallationFinished) { actualInitializingPage = [ () => _stepServerProviderToken(cubit), - () => _stepServerType(cubit, volumeCubit), + () => _stepServerType(cubit), () => _stepDnsProviderToken(cubit), () => _stepBackblaze(cubit), () => _stepDomain(cubit), @@ -228,7 +226,6 @@ class InitializingPage extends StatelessWidget { Widget _stepServerType( final ServerInstallationCubit serverInstallationCubit, - final ApiProviderVolumeCubit apiProviderVolumeCubit, ) => BlocProvider( create: (final context) => @@ -236,7 +233,6 @@ 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 950b0f4d..cb9b496a 100644 --- a/lib/ui/pages/setup/initializing/server_type_picker.dart +++ b/lib/ui/pages/setup/initializing/server_type_picker.dart @@ -3,7 +3,6 @@ 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'; @@ -14,12 +13,10 @@ 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(); @@ -29,7 +26,12 @@ class _ServerTypePickerState extends State { ServerProviderLocation? serverProviderLocation; ServerType? serverType; - void setServerProviderLocation(final ServerProviderLocation? location) { + void setServerProviderLocation(final ServerProviderLocation? location) async { + if (location != null) { + await widget.serverInstallationCubit.setLocationIdentifier( + location.identifier, + ); + } setState(() { serverProviderLocation = location; }); @@ -47,7 +49,6 @@ class _ServerTypePickerState extends State { return SelectTypePage( location: serverProviderLocation!, serverInstallationCubit: widget.serverInstallationCubit, - apiProviderVolumeCubit: widget.apiProviderVolumeCubit, backToLocationPickingCallback: () { setServerProviderLocation(null); }, @@ -150,24 +151,23 @@ 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) { final Future> serverTypes = serverInstallationCubit.fetchAvailableTypesByLocation(location); - final Future pricePerGb = apiProviderVolumeCubit.getPricePerGb(); + final Future prices = + serverInstallationCubit.fetchAvailableAdditionalPricing(); return FutureBuilder( future: Future.wait([ serverTypes, - pricePerGb, + prices, ]), builder: ( final BuildContext context, @@ -175,7 +175,7 @@ class SelectTypePage extends StatelessWidget { ) { if (snapshot.hasData) { if ((snapshot.data![0] as List).isEmpty || - (snapshot.data![1] as Price?) == null) { + (snapshot.data![1] as AdditionalPricing?) == null) { return Column( crossAxisAlignment: CrossAxisAlignment.start, 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( topChild: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -325,7 +329,7 @@ class SelectTypePage extends StatelessWidget { '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}' + '${(type.price.value + storagePrice + publicIpPrice).toStringAsFixed(4)} ${type.price.currency.shortcode}' ], ), style: Theme.of(context) @@ -334,29 +338,32 @@ class SelectTypePage extends StatelessWidget { ), ], ), - 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}', - ], - ), - style: Theme.of(context) - .textTheme - .bodyLarge, - ), - ], + Text( + 'initializing.choose_server_type_payment_server' + .tr( + args: [type.price.value.toString()], + ), + style: + Theme.of(context).textTheme.bodyMedium, ), + 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, + ), ], ), ),