From 234064ed72820d6721279b267c94ae7e86d51d6c Mon Sep 17 00:00:00 2001 From: NaiJi Date: Tue, 9 May 2023 03:15:48 -0300 Subject: [PATCH] feat: Implement infrastructure for new DNS provider deSEC --- assets/images/logos/desec.svg | 89 +++++ lib/config/hive_config.dart | 3 + .../rest_maps/api_factory_creator.dart | 3 + .../rest_maps/dns_providers/desec/desec.dart | 287 ++++++++++++++++ .../dns_providers/desec/desec_factory.dart | 16 + .../server_installation_cubit.dart | 9 + .../server_installation_repository.dart | 4 + lib/logic/get_it/api_config.dart | 10 + lib/logic/models/hive/server_domain.dart | 2 + .../initializing/dns_provider_picker.dart | 307 ++++++++++++++++++ .../setup/initializing/initializing.dart | 60 +--- 11 files changed, 742 insertions(+), 48 deletions(-) create mode 100644 assets/images/logos/desec.svg create mode 100644 lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart create mode 100644 lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart create mode 100644 lib/ui/pages/setup/initializing/dns_provider_picker.dart diff --git a/assets/images/logos/desec.svg b/assets/images/logos/desec.svg new file mode 100644 index 00000000..cb54b268 --- /dev/null +++ b/assets/images/logos/desec.svg @@ -0,0 +1,89 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index b6ba018c..44b03f26 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -93,6 +93,9 @@ class BNames { /// A String field of [serverInstallationBox] box. static String serverProvider = 'serverProvider'; + /// A String field of [serverInstallationBox] box. + static String dnsProvider = 'dnsProvider'; + /// A String field of [serverLocation] box. static String serverLocation = 'serverLocation'; diff --git a/lib/logic/api_maps/rest_maps/api_factory_creator.dart b/lib/logic/api_maps/rest_maps/api_factory_creator.dart index 25518f3c..c1762429 100644 --- a/lib/logic/api_maps/rest_maps/api_factory_creator.dart +++ b/lib/logic/api_maps/rest_maps/api_factory_creator.dart @@ -1,5 +1,6 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/api_factory_settings.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/cloudflare/cloudflare_factory.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/digital_ocean/digital_ocean_factory.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/server_providers/hetzner/hetzner_factory.dart'; @@ -30,6 +31,8 @@ class ApiFactoryCreator { final DnsProviderApiFactorySettings settings, ) { switch (settings.provider) { + case DnsProvider.desec: + return DesecApiFactory(); case DnsProvider.cloudflare: return CloudflareApiFactory(); case DnsProvider.unknown: diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart new file mode 100644 index 00000000..1906be55 --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec.dart @@ -0,0 +1,287 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; + +class DesecApi extends DnsProviderApi { + DesecApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + + final String? customToken; + + @override + RegExp getApiTokenValidation() => + RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); + + @override + BaseOptions get options { + final BaseOptions options = BaseOptions(baseUrl: rootAddress); + if (isWithToken) { + final String? token = getIt().cloudFlareKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; + } + + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; + } + + @override + String rootAddress = 'https://desec.io/api/v1/domains/'; + + @override + Future> isApiTokenValid(final String token) async { + bool isValid = false; + Response? response; + String message = ''; + final Dio client = await getClient(); + try { + response = await client.get( + '', + options: Options( + followRedirects: false, + validateStatus: (final status) => + status != null && (status >= 200 || status == 401), + headers: {'Authorization': 'Token $token'}, + ), + ); + } catch (e) { + print(e); + isValid = false; + message = e.toString(); + } finally { + close(client); + } + + if (response == null) { + return APIGenericResult( + data: isValid, + success: false, + message: message, + ); + } + + if (response.statusCode == HttpStatus.ok) { + isValid = true; + } else if (response.statusCode == HttpStatus.unauthorized) { + isValid = false; + } else { + throw Exception('code: ${response.statusCode}'); + } + + return APIGenericResult( + data: isValid, + success: true, + message: response.statusMessage, + ); + } + + @override + Future getZoneId(final String domain) async => domain; + + @override + Future> removeSimilarRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final String domainName = domain.domainName; + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + final Response response = await client.get(url); + + final List records = response.data['result'] ?? []; + await client.put(url, data: records); + } catch (e) { + print(e); + return APIGenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return APIGenericResult(success: true, data: null); + } + + @override + Future> getDnsRecords({ + required final ServerDomain domain, + }) async { + Response response; + final String domainName = domain.domainName; + final List allRecords = []; + + final String url = '/$domainName/rrsets/'; + + final Dio client = await getClient(); + try { + response = await client.get(url); + final List records = response.data['result'] ?? []; + + for (final record in records) { + allRecords.add( + DnsRecord( + name: record['subname'], + type: record['type'], + content: record['records'], + ttl: record['ttl'], + ), + ); + } + } catch (e) { + print(e); + } finally { + close(client); + } + + return allRecords; + } + + @override + Future> createMultipleDnsRecords({ + required final ServerDomain domain, + final String? ip4, + }) async { + final String domainName = domain.domainName; + final List listDnsRecords = projectDnsRecords(domainName, ip4); + final List allCreateFutures = []; + + final Dio client = await getClient(); + try { + for (final DnsRecord record in listDnsRecords) { + allCreateFutures.add( + client.post( + '/$domainName/rrsets/', + data: record.toJson(), + ), + ); + } + await Future.wait(allCreateFutures); + } on DioError catch (e) { + print(e.message); + rethrow; + } catch (e) { + print(e); + return APIGenericResult( + success: false, + data: null, + message: e.toString(), + ); + } finally { + close(client); + } + + return APIGenericResult(success: true, data: null); + } + + List projectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = + DnsRecord(type: 'A', name: domainName, content: ip4); + + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + + final DnsRecord txt1 = DnsRecord( + type: 'TXT', + name: '_dmarc', + content: 'v=DMARC1; p=none', + ttl: 18000, + ); + + final DnsRecord txt2 = DnsRecord( + type: 'TXT', + name: domainName, + content: 'v=spf1 a mx ip4:$ip4 -all', + ttl: 18000, + ); + + return [ + domainA, + apiA, + cloudA, + gitA, + meetA, + passwordA, + socialA, + mx, + txt1, + txt2, + vpn + ]; + } + + @override + Future setDnsRecord( + final DnsRecord record, + final ServerDomain domain, + ) async { + final String domainZoneId = domain.zoneId; + final String url = '$rootAddress/zones/$domainZoneId/dns_records'; + + final Dio client = await getClient(); + try { + await client.post( + url, + data: record.toJson(), + ); + } catch (e) { + print(e); + } finally { + close(client); + } + } + + @override + Future> domainList() async { + final String url = '$rootAddress/zones'; + List domains = []; + + final Dio client = await getClient(); + try { + final Response response = await client.get( + url, + queryParameters: {'per_page': 50}, + ); + domains = response.data['result'] + .map((final el) => el['name'] as String) + .toList(); + } catch (e) { + print(e); + } finally { + close(client); + } + + return domains; + } +} diff --git a/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart new file mode 100644 index 00000000..6c10259b --- /dev/null +++ b/lib/logic/api_maps/rest_maps/dns_providers/desec/desec_factory.dart @@ -0,0 +1,16 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desec/desec.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_api_settings.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/dns_provider_factory.dart'; + +class DesecApiFactory extends DnsProviderApiFactory { + @override + DnsProviderApi getDnsProvider({ + final DnsProviderApiSettings settings = const DnsProviderApiSettings(), + }) => + DesecApi( + hasLogger: settings.hasLogger, + isWithToken: settings.isWithToken, + customToken: settings.customToken, + ); +} diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 5638b765..06df6d5a 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -66,6 +66,15 @@ class ServerInstallationCubit extends Cubit { ); } + void setDnsProviderType(final DnsProvider providerType) async { + await repository.saveDnsProviderType(providerType); + ApiController.initDnsProviderApiFactory( + DnsProviderApiFactorySettings( + provider: providerType, + ), + ); + } + ProviderApiTokenValidation serverProviderApiTokenValidation() => ApiController.currentServerProviderApiFactory! .getServerProvider() diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index 5d45e7b9..851b2be5 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -706,6 +706,10 @@ class ServerInstallationRepository { getIt().init(); } + Future saveDnsProviderType(final DnsProvider type) async { + await getIt().storeDnsProviderType(type); + } + Future saveBackblazeKey( final BackblazeCredential backblazeCredential, ) async { diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 434c9b32..11d73a85 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -14,6 +14,8 @@ class ApiConfigModel { String? get serverType => _serverType; String? get cloudFlareKey => _cloudFlareKey; ServerProvider? get serverProvider => _serverProvider; + DnsProvider? get dnsProvider => _dnsProvider; + BackblazeCredential? get backblazeCredential => _backblazeCredential; ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; @@ -23,6 +25,7 @@ class ApiConfigModel { String? _cloudFlareKey; String? _serverType; ServerProvider? _serverProvider; + DnsProvider? _dnsProvider; ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; ServerDomain? _serverDomain; @@ -33,6 +36,11 @@ class ApiConfigModel { _serverProvider = value; } + Future storeDnsProviderType(final DnsProvider value) async { + await _box.put(BNames.dnsProvider, value); + _dnsProvider = value; + } + Future storeServerProviderKey(final String value) async { await _box.put(BNames.hetznerKey, value); _serverProviderKey = value; @@ -75,6 +83,7 @@ class ApiConfigModel { void clear() { _serverProviderKey = null; + _dnsProvider = null; _serverLocation = null; _cloudFlareKey = null; _backblazeCredential = null; @@ -95,5 +104,6 @@ class ApiConfigModel { _backblazeBucket = _box.get(BNames.backblazeBucket); _serverType = _box.get(BNames.serverTypeIdentifier); _serverProvider = _box.get(BNames.serverProvider); + _dnsProvider = _box.get(BNames.dnsProvider); } } diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart index 9b5d32c1..913fcd42 100644 --- a/lib/logic/models/hive/server_domain.dart +++ b/lib/logic/models/hive/server_domain.dart @@ -29,4 +29,6 @@ enum DnsProvider { unknown, @HiveField(1) cloudflare, + @HiveField(2) + desec } diff --git a/lib/ui/pages/setup/initializing/dns_provider_picker.dart b/lib/ui/pages/setup/initializing/dns_provider_picker.dart new file mode 100644 index 00000000..cb0d2111 --- /dev/null +++ b/lib/ui/pages/setup/initializing/dns_provider_picker.dart @@ -0,0 +1,307 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/dns_provider_form_cubit.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/ui/components/buttons/brand_button.dart'; +import 'package:selfprivacy/ui/components/buttons/outlined_button.dart'; +import 'package:selfprivacy/ui/components/cards/outlined_card.dart'; +import 'package:selfprivacy/utils/launch_url.dart'; + +class DnsProviderPicker extends StatefulWidget { + const DnsProviderPicker({ + required this.formCubit, + required this.serverInstallationCubit, + super.key, + }); + + final DnsProviderFormCubit formCubit; + final ServerInstallationCubit serverInstallationCubit; + + @override + State createState() => _DnsProviderPickerState(); +} + +class _DnsProviderPickerState extends State { + DnsProvider selectedProvider = DnsProvider.unknown; + + void setProvider(final DnsProvider provider) { + setState(() { + selectedProvider = provider; + }); + } + + @override + Widget build(final BuildContext context) { + switch (selectedProvider) { + case DnsProvider.unknown: + return ProviderSelectionPage( + serverInstallationCubit: widget.serverInstallationCubit, + callback: setProvider, + ); + + case DnsProvider.cloudflare: + return ProviderInputDataPage( + providerCubit: widget.formCubit, + providerInfo: ProviderPageInfo( + providerType: DnsProvider.cloudflare, + pathToHow: 'how_cloudflare', + image: Image.asset( + 'assets/images/logos/cloudflare.png', + width: 150, + ), + ), + ); + + case DnsProvider.desec: + return ProviderInputDataPage( + providerCubit: widget.formCubit, + providerInfo: ProviderPageInfo( + providerType: DnsProvider.desec, + pathToHow: 'how_digital_ocean_dns', + image: Image.asset( + 'assets/images/logos/digital_ocean.png', + width: 150, + ), + ), + ); + } + } +} + +class ProviderPageInfo { + const ProviderPageInfo({ + required this.providerType, + required this.pathToHow, + required this.image, + }); + + final String pathToHow; + final Image image; + final DnsProvider providerType; +} + +class ProviderInputDataPage extends StatelessWidget { + const ProviderInputDataPage({ + required this.providerInfo, + required this.providerCubit, + super.key, + }); + + final ProviderPageInfo providerInfo; + final DnsProviderFormCubit providerCubit; + + @override + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.connect_to_dns'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.connect_to_server_provider_text'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + CubitFormTextField( + formFieldCubit: providerCubit.apiKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Provider API Token', + ), + ), + const SizedBox(height: 32), + BrandButton.rised( + text: 'basis.connect'.tr(), + onPressed: () => providerCubit.trySubmit(), + ), + const SizedBox(height: 10), + BrandOutlinedButton( + child: Text('initializing.how'.tr()), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + BrandMarkdown( + fileName: providerInfo.pathToHow, + ), + ], + ), + ), + ), + ), + ], + ); +} + +class ProviderSelectionPage extends StatelessWidget { + const ProviderSelectionPage({ + required this.callback, + required this.serverInstallationCubit, + super.key, + }); + + final Function callback; + final ServerInstallationCubit serverInstallationCubit; + + @override + Widget build(final BuildContext context) => SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'initializing.select_dns'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 10), + Text( + 'initializing.select_provider'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 10), + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color.fromARGB(255, 241, 215, 166), + ), + child: SvgPicture.asset( + 'assets/images/logos/cloudflare.svg', + ), + ), + const SizedBox(width: 16), + Text( + 'Hetzner Cloud', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_free'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_payment_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_payment_text_cloudflare' + .tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.rised( + text: 'basis.select'.tr(), + onPressed: () { + serverInstallationCubit + .setDnsProviderType(DnsProvider.cloudflare); + callback(DnsProvider.cloudflare); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => + launchURL('https://dash.cloudflare.com/'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + OutlinedCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 40, + height: 40, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(40), + color: const Color.fromARGB(255, 245, 229, 82), + ), + child: SvgPicture.asset( + 'assets/images/logos/desec.svg', + ), + ), + const SizedBox(width: 16), + Text( + 'deSEC', + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_price_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_price_free'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Text( + 'initializing.select_provider_payment_title'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'initializing.select_provider_payment_text_do'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + BrandButton.rised( + text: 'basis.select'.tr(), + onPressed: () { + serverInstallationCubit + .setDnsProviderType(DnsProvider.desec); + callback(DnsProvider.desec); + }, + ), + // Outlined button that will open website + BrandOutlinedButton( + onPressed: () => launchURL('https://desec.io/'), + title: 'initializing.select_provider_site_button'.tr(), + ), + ], + ), + ), + ), + ], + ), + ); +} diff --git a/lib/ui/pages/setup/initializing/initializing.dart b/lib/ui/pages/setup/initializing/initializing.dart index 199203c3..749fbdc9 100644 --- a/lib/ui/pages/setup/initializing/initializing.dart +++ b/lib/ui/pages/setup/initializing/initializing.dart @@ -17,6 +17,7 @@ import 'package:selfprivacy/ui/components/drawers/progress_drawer.dart'; import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; import 'package:selfprivacy/ui/components/drawers/support_drawer.dart'; import 'package:selfprivacy/ui/layouts/responsive_layout_with_infobox.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing/dns_provider_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_provider_picker.dart'; import 'package:selfprivacy/ui/pages/setup/initializing/server_type_picker.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; @@ -39,7 +40,7 @@ class InitializingPage extends StatelessWidget { actualInitializingPage = [ () => _stepServerProviderToken(cubit), () => _stepServerType(cubit), - () => _stepCloudflare(cubit), + () => _stepDnsProviderToken(cubit), () => _stepBackblaze(cubit), () => _stepDomain(cubit), () => _stepUser(cubit), @@ -238,56 +239,19 @@ class InitializingPage extends StatelessWidget { ), ); - Widget _stepCloudflare(final ServerInstallationCubit initializingCubit) => + Widget _stepDnsProviderToken( + final ServerInstallationCubit initializingCubit, + ) => BlocProvider( create: (final context) => DnsProviderFormCubit(initializingCubit), child: Builder( - builder: (final context) => ResponsiveLayoutWithInfobox( - topChild: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${'initializing.connect_to_server_provider'.tr()}Cloudflare', - style: Theme.of(context).textTheme.headlineSmall, - ), - const SizedBox(height: 16), - Text( - 'initializing.manage_domain_dns'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ), - primaryColumn: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CubitFormTextField( - formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: const EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'initializing.cloudflare_api_token'.tr(), - ), - ), - const SizedBox(height: 32), - BrandButton.filled( - onPressed: () => - context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - const SizedBox(height: 10), - BrandOutlinedButton( - onPressed: () { - context.read().showArticle( - article: 'how_cloudflare', - context: context, - ); - Scaffold.of(context).openEndDrawer(); - }, - title: 'initializing.how'.tr(), - ), - ], - ), - ), + builder: (final context) { + final providerCubit = context.watch(); + return DnsProviderPicker( + formCubit: providerCubit, + serverInstallationCubit: initializingCubit, + ); + }, ), );