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",
"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",

View File

@ -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 хранилище неверна",

View File

@ -321,8 +321,8 @@ class HetznerApi extends RestApiMap {
return GenericResult(success: true, data: null);
}
Future<GenericResult<double?>> getPricePerGb() async {
double? price;
Future<GenericResult<HetznerPricing?>> 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<GenericResult<List<HetznerVolume>>> getVolumes({

View File

@ -26,8 +26,17 @@ class ApiProviderVolumeCubit
}
}
Future<Price?> getPricePerGb() async =>
(await ProvidersController.currentServerProvider!.getPricePerGb()).data;
Future<Price?> getPricePerGb() async {
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 {
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/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<ServerInstallationState> {
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 {
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 {
await repository.saveServerType(serverType);
await ProvidersController.currentServerProvider!
.trySetServerLocation(serverType.location.identifier);
emit(
(state as ServerInstallationNotFinished).copyWith(
serverTypeIdentificator: serverType.identifier,

View File

@ -190,3 +190,22 @@ class HetznerVolume {
static HetznerVolume fromJson(final Map<String, dynamic> 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? 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
Future<GenericResult<Price?>> getPricePerGb() async => GenericResult(
Future<GenericResult<AdditionalPricing?>> 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<DigitalOceanVolume> volumes = resultVolumes.data;
final Price pricePerGb = resultPricePerGb.data!;
try {
final Price pricePerGb = resultPricePerGb.data!.perVolumeGb;
final droplet = servers.firstWhere(
(final server) => server['id'] == serverId,
);

View File

@ -545,8 +545,8 @@ class HetznerServerProvider extends ServerProvider {
}
@override
Future<GenericResult<Price?>> getPricePerGb() async {
final result = await _adapter.api().getPricePerGb();
Future<GenericResult<AdditionalPricing?>> 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<HetznerServerInfo> servers = resultServers.data;
final List<HetznerVolume> 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,

View File

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

View File

@ -45,6 +45,7 @@ class _ExtendingVolumePageState extends State<ExtendingVolumePage> {
late double _currentSliderGbValue;
double _pricePerGb = 1.0;
// TODO: Wtfff hardcode?!?!?
final DiskSize maxSize = const DiskSize(byte: 500000000000);
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: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<ServerInstallationCubit>();
final volumeCubit = context.read<ApiProviderVolumeCubit>();
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,
),
),
);

View File

@ -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<ServerTypePicker> createState() => _ServerTypePickerState();
@ -29,7 +26,12 @@ class _ServerTypePickerState extends State<ServerTypePicker> {
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<ServerTypePicker> {
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<List<ServerType>> serverTypes =
serverInstallationCubit.fetchAvailableTypesByLocation(location);
final Future<Price?> pricePerGb = apiProviderVolumeCubit.getPricePerGb();
final Future<AdditionalPricing?> 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<ServerType>).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,
),
],
),
),