diff --git a/packages/dart/npt_flutter/assets/onboarding_bg.svg b/packages/dart/npt_flutter/assets/onboarding_bg.svg new file mode 100644 index 000000000..c925206fa --- /dev/null +++ b/packages/dart/npt_flutter/assets/onboarding_bg.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dart/npt_flutter/lib/app.dart b/packages/dart/npt_flutter/lib/app.dart index 72ccacfd4..2a19be78e 100644 --- a/packages/dart/npt_flutter/lib/app.dart +++ b/packages/dart/npt_flutter/lib/app.dart @@ -32,6 +32,8 @@ class App extends StatelessWidget { ], child: MultiBlocProvider( providers: [ + // TODO this should be called LocalSettingsCubit and move + // Localization from the SettingsCubit to this BlocProvider( create: (_) => EnableLoggingCubit(), ), @@ -42,7 +44,7 @@ class App extends StatelessWidget { create: (_) => LogsCubit(), ), - /// A cubit which manages the onboarding status + // A bloc which manages the atDirectory state BlocProvider( create: (_) => OnboardingCubit(), ), diff --git a/packages/dart/npt_flutter/lib/constants.dart b/packages/dart/npt_flutter/lib/constants.dart index 58c1eb465..d412655ab 100644 --- a/packages/dart/npt_flutter/lib/constants.dart +++ b/packages/dart/npt_flutter/lib/constants.dart @@ -1,5 +1,6 @@ +import 'package:flutter/material.dart'; + class Constants { - static const rootDomain = 'root.atsign.org'; static String? get namespace => 'noports'; // TODO: issue & secure API key properly static String? get appAPIKey => 'asdf'; @@ -15,5 +16,10 @@ class Constants { "@rv_ap": "Singapore", }; + static Map getRootDomains(BuildContext context) { + // TODO localize right hand side of map + return {'root.atsign.org': 'Default (Prod)', 'vip.ve.atsign.zone': 'Demo (VE)'}; + } + static const languages = ['English', 'Spanish', 'Br portuguese', 'Mandarin', 'Cantonese']; } diff --git a/packages/dart/npt_flutter/lib/features/favorite/bloc/favorite_bloc.dart b/packages/dart/npt_flutter/lib/features/favorite/bloc/favorite_bloc.dart index 0401e3f9d..1f6bd41ce 100644 --- a/packages/dart/npt_flutter/lib/features/favorite/bloc/favorite_bloc.dart +++ b/packages/dart/npt_flutter/lib/features/favorite/bloc/favorite_bloc.dart @@ -15,6 +15,8 @@ class FavoriteBloc extends LoggingBloc { on(_onRemove); } + void clearAll() => emit(const FavoritesInitial()); + FutureOr _onLoad( FavoriteLoadEvent event, Emitter emit) async { emit(const FavoritesLoading()); diff --git a/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_cubit.dart b/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_cubit.dart index 65f4a9bbc..890e5a93b 100644 --- a/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_cubit.dart +++ b/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_cubit.dart @@ -1,10 +1,49 @@ -import 'package:npt_flutter/features/logging/logging.dart'; - -part 'onboarding_state.dart'; +import 'package:npt_flutter/features/logging/models/logging_bloc.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; class OnboardingCubit extends LoggingCubit { - OnboardingCubit() : super(const OnboardingInitial()); + OnboardingCubit() + : super(const OnboardingState(atSign: '', status: OnboardingStatus.offboarded, rootDomain: 'root.atsign.org')); + + void setRootDomain(String rootDomain) => + emit(OnboardingState(atSign: state.atSign, status: state.status, rootDomain: rootDomain)); + String getRootDomain() => (state.rootDomain); + + void setAtSign(String atSign) => + emit(OnboardingState(atSign: atSign, status: state.status, rootDomain: state.rootDomain)); + String getAtSign() => (state.atSign); + + void setStatus(OnboardingStatus status) => + emit(OnboardingState(atSign: state.atSign, status: status, rootDomain: state.rootDomain)); + OnboardingStatus getStatus() => (state.status); + + /// If state is passed, all other arguments are ignored + /// If individual arguments (atsign, rootDomain, status) are passed + /// then they will override the value of the current state + /// keeping unspecified values the same + void setState({ + String? atSign, + OnboardingStatus? status, + String? rootDomain, + }) => + emit(OnboardingState( + atSign: atSign ?? state.atSign, + status: status ?? state.status, + rootDomain: rootDomain ?? state.rootDomain, + )); +} + +enum OnboardingStatus { onboarded, offboarded } + +class OnboardingState extends AtsignInformation { + final OnboardingStatus status; + const OnboardingState({required this.status, required super.atSign, required super.rootDomain}); + + @override + List get props => [atSign, status, rootDomain]; - void onboard(String atSign) => emit(Onboarded(atSign)); - void offboard() => emit(const OnboardingInitial()); + @override + String toString() { + return 'OnboardingState($atSign, ${status.name}, $rootDomain)'; + } } diff --git a/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_state.dart b/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_state.dart deleted file mode 100644 index 760bf8f38..000000000 --- a/packages/dart/npt_flutter/lib/features/onboarding/cubit/onboarding_state.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'onboarding_cubit.dart'; - -sealed class OnboardingState extends Loggable { - const OnboardingState(); - - @override - List get props => []; -} - -final class OnboardingInitial extends OnboardingState { - const OnboardingInitial(); - - @override - String toString() { - return 'OnboardingInitial'; - } -} - -final class Onboarded extends OnboardingState { - final String atSign; - const Onboarded(this.atSign); - - @override - List get props => [atSign]; - - @override - String toString() { - return 'Onboarded($atSign)'; - } -} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/util/atsign_manager.dart b/packages/dart/npt_flutter/lib/features/onboarding/util/atsign_manager.dart new file mode 100644 index 000000000..1290e5b8e --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/util/atsign_manager.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:npt_flutter/app.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class AtsignInformation extends Loggable { + final String atSign; + final String rootDomain; + + const AtsignInformation({required this.atSign, required this.rootDomain}); + + Map toJson() => { + "atsign": atSign, + "root-domain": rootDomain, + }; + + static AtsignInformation? fromJson(Map json) { + if (json["atsign"] is! String || json["root-domain"] is! String) { + return null; + } + return AtsignInformation( + atSign: json["atsign"], + rootDomain: json["root-domain"], + ); + } + + @override + List get props => [atSign, rootDomain]; + + @override + String toString() { + return 'AtsignInformation($atSign, $rootDomain)'; + } +} + +// This will return a map which looks like: +// +// { +// "@alice": AtsignInformation{ atSign: "@alice", rootDomain: "root.atsign.org" }, +// "@bob": AtsignInformation{ atSign: "@alice", rootDomain: "vip.ve.atsign.zone" }, +// } +// +// Note: AtsignInformation is a class, so usage will look like +// +// var atSign = "@alice"; +// var atSignInfo = await getAtsignEntries(); +// var rootDomain = atSignInfo[atSign].rootDomain; +// +// Now you have the rootDomain for the existing atSign and can use it to onboard +// correctly + +Future> getAtsignEntries() async { + var keychainAtSigns = await KeychainUtil.getAtsignList() ?? []; + var atSignInfo = []; + try { + atSignInfo = await _getAtsignInformationFromFile(); + } catch (e) { + App.log( + "Failed get Atsign Information, ignoring invalid file: ${e.toString()}".loggable, + ); + return {}; + } + var atSignMap = {}; + for (var item in atSignInfo) { + if (keychainAtSigns.contains(item.atSign)) { + atSignMap[item.atSign] = item; + } + } + return atSignMap; +} + +// This class will allow you to store atSign information +// you need to call this after onboarding a NEW atSign +Future saveAtsignInformation(AtsignInformation info) async { + var f = await _getAtsignInformationFile(); + final List atSignInfo; + try { + atSignInfo = await _getAtsignInformationFromFile(f); + } catch (e) { + // We only end up here if we failed to create, get, or read the file + // we don't want to overwrite it in that scenario, so return false + // + // We won't end up here if it was a json parse error, such as invalid + // json, we do want to overwrite that so that the app can recover as best + // as possible + return false; + } + if (f == null) return false; + + // Replace the existing entry with the new one if it exists + bool found = false; + for (int i = 0; i < atSignInfo.length; i++) { + if (atSignInfo[i].atSign == info.atSign) { + found = true; + atSignInfo[i] = info; + } + } + // Otherwise add it as a new entry + if (!found) { + atSignInfo.add(info); + } + try { + f.writeAsString( + jsonEncode(atSignInfo.map((e) => e.toJson()).toList()), + mode: FileMode.writeOnly, + flush: true, + ); + return true; + } catch (e) { + App.log( + "Failed to write Atsign Information to file: ${e.toString()}".loggable, + ); + return false; + } +} + +// Does not throw, returns null if it can't get / create the file +Future _getAtsignInformationFile() async { + final Directory dir; + try { + dir = await getApplicationSupportDirectory(); + dir.create(recursive: true); // This checks if it exists internally + } catch (e) { + App.log( + "Failed to Get Application Support Directory: ${e.toString()}".loggable, + ); + return null; + } + final f = File(p.join(dir.path, "atsign_information.json")); + try { + if (!await f.exists()) { + f.create(recursive: true); + } + return f; + } catch (e) { + App.log( + "Failed to Get Atsign Information File : ${e.toString()}".loggable, + ); + return null; + } +} + +Future> _getAtsignInformationFromFile([File? f]) async { + f ??= await _getAtsignInformationFile(); + if (f == null) throw Exception("Failed to get the Atsign Information File"); + try { + var contents = await f.readAsString(); + if (contents.trim().isEmpty) return []; + var json = jsonDecode(contents); + if (json is! Iterable) { + return []; // The file format is invalid so return as a non-error and we will overwrite it + } + var res = []; + for (var item in json) { + if (item is! Map) continue; + var info = AtsignInformation.fromJson(item); + if (info == null) continue; + res.add(info); + } + return res; + } catch (e) { + App.log( + "Failed to Parse Atsign Information File : ${e.toString()}".loggable, + ); + rethrow; + } +} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/util/post_onboard.dart b/packages/dart/npt_flutter/lib/features/onboarding/util/post_onboard.dart index 16777c09b..4e9ebeee8 100644 --- a/packages/dart/npt_flutter/lib/features/onboarding/util/post_onboard.dart +++ b/packages/dart/npt_flutter/lib/features/onboarding/util/post_onboard.dart @@ -2,16 +2,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:npt_flutter/app.dart'; import 'package:npt_flutter/features/features.dart'; -Future postOnboard(String atSign) async { - App.navState.currentContext?.read().onboard(atSign); +Future postOnboard(String atSign, String rootDomain) async { + App.navState.currentContext?.read().setState( + atSign: atSign, + rootDomain: rootDomain, + status: OnboardingStatus.onboarded, + ); // Start loading application data in the background as soon as we have an atClient - App.navState.currentContext - ?.read() - .add(const ProfileListLoadEvent()); - App.navState.currentContext - ?.read() - .add(const SettingsLoadEvent()); - App.navState.currentContext - ?.read() - .add(const FavoriteLoadEvent()); + App.navState.currentContext?.read().add(const ProfileListLoadEvent()); + App.navState.currentContext?.read().add(const SettingsLoadEvent()); + App.navState.currentContext?.read().add(const FavoriteLoadEvent()); } diff --git a/packages/dart/npt_flutter/lib/features/onboarding/util/pre_offboard.dart b/packages/dart/npt_flutter/lib/features/onboarding/util/pre_offboard.dart new file mode 100644 index 000000000..a26eb76e8 --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/util/pre_offboard.dart @@ -0,0 +1,21 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:npt_flutter/app.dart'; +import 'package:npt_flutter/features/features.dart'; + +// Hand this method the atSign you wish to offboard +// Returns: a boolean, true = success, false = failed +Future preSignout() async { + App.log("Resetting all application state before signout".loggable); + // We need to do the following before "signing out" + // - Wipe all application state + App.navState.currentContext?.read().stopAllAndClear(); + App.navState.currentContext?.read().clear(); + App.navState.currentContext?.read().deselectAll(); + App.navState.currentContext?.read().clearAll(); + App.navState.currentContext?.read().clearAll(); + App.navState.currentContext?.read().clear(); + App.navState.currentContext?.read().setStatus(OnboardingStatus.offboarded); + // - Reset the tray icon + App.navState.currentContext?.read().initialize(); + return true; +} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/view/onboarding_view.dart b/packages/dart/npt_flutter/lib/features/onboarding/view/onboarding_view.dart new file mode 100644 index 000000000..b9581ba05 --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/view/onboarding_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:npt_flutter/features/onboarding/widgets/onboarding_button.dart'; +import 'package:npt_flutter/styles/sizes.dart'; +import 'package:npt_flutter/widgets/custom_text_button.dart'; + +class OnboardingView extends StatelessWidget { + const OnboardingView({super.key}); + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + final textTheme = Theme.of(context).textTheme; + return Stack( + children: [ + Positioned.fill( + child: SvgPicture.asset( + 'assets/onboarding_bg.svg', + fit: BoxFit.cover, + ), + ), + Align( + child: Column( + children: [ + gapH108, + Text( + strings.onboardingTitle, + style: textTheme.headlineLarge!.copyWith( + color: Colors.black, + ), + ), + Text(strings.onboardingSubTitle, style: textTheme.headlineMedium), + gapH20, + const OnboardingButton(), + ], + ), + ), + const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only( + bottom: Sizes.p44, + right: Sizes.p44, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CustomTextButton.resetAtsign(), + ], + ), + ), + ) + ], + ); + } +} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/widgets/at_directory_selector.dart b/packages/dart/npt_flutter/lib/features/onboarding/widgets/at_directory_selector.dart new file mode 100644 index 000000000..ef2a22f60 --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/widgets/at_directory_selector.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:npt_flutter/constants.dart'; +import 'package:npt_flutter/features/onboarding/cubit/onboarding_cubit.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; + +class AtDirectorySelector extends StatefulWidget { + const AtDirectorySelector({ + required this.options, + super.key, + }); + final Map options; + + @override + State createState() => _AtDirectorySelectorState(); +} + +class _AtDirectorySelectorState extends State { + final focusNode = FocusNode(); + final controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final rootDomains = Constants.getRootDomains(context); + return BlocBuilder(builder: (context, state) { + controller.value = TextEditingValue(text: state.rootDomain); + return TextFormField( + enabled: !widget.options.containsKey(state.atSign), + controller: controller, + onChanged: (rootDomain) { + context.read().setRootDomain(rootDomain); + }, + decoration: InputDecoration( + /// This menuAnchor is a dropdown button that allows you to quickly select + /// existing values from [options] + suffixIcon: rootDomains.isNotEmpty + ? Directionality( + textDirection: TextDirection.rtl, + child: MenuAnchor( + style: const MenuStyle(alignment: AlignmentDirectional.bottomStart), + childFocusNode: focusNode, + menuChildren: rootDomains.entries.map((e) { + return Directionality( + textDirection: TextDirection.ltr, + child: MenuItemButton( + child: Text(e.value), + onPressed: () { + context.read().setRootDomain(e.key); + }, + ), + ); + }).toList(), + builder: (BuildContext context, MenuController controller, Widget? child) { + return IconButton( + focusNode: focusNode, + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.arrow_drop_down), + ); + }, + ), + ) + : null, + ), + ); + }); + } +} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/widgets/atsign_selector.dart b/packages/dart/npt_flutter/lib/features/onboarding/widgets/atsign_selector.dart new file mode 100644 index 000000000..92464dd73 --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/widgets/atsign_selector.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:npt_flutter/features/onboarding/cubit/onboarding_cubit.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; + +class AtsignSelector extends StatefulWidget { + const AtsignSelector({ + required this.options, + super.key, + }); + final Map options; + @override + State createState() => _AtsignSelectorState(); +} + +class _AtsignSelectorState extends State { + final focusNode = FocusNode(); + final controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + controller.value = TextEditingValue(text: state.atSign); + return TextFormField( + controller: controller, + onChanged: (atsign) { + context.read().setState( + atSign: atsign, + rootDomain: widget.options[atsign]?.rootDomain, + ); + }, + decoration: InputDecoration( + /// This menuAnchor is a dropdown button that allows you to quickly select + /// existing values from [options] + suffixIcon: widget.options.isNotEmpty + ? Directionality( + textDirection: TextDirection.rtl, + child: MenuAnchor( + style: const MenuStyle(alignment: AlignmentDirectional.bottomStart), + childFocusNode: focusNode, + menuChildren: widget.options.keys.map((atsign) { + return Directionality( + textDirection: TextDirection.ltr, + child: MenuItemButton( + child: Text(atsign), + onPressed: () { + context.read().setState( + atSign: atsign, + rootDomain: widget.options[atsign]?.rootDomain, + ); + }, + ), + ); + }).toList(), + builder: (BuildContext context, MenuController controller, Widget? child) { + return IconButton( + focusNode: focusNode, + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.arrow_drop_down), + ); + }, + ), + ) + : null, + ), + ); + }); + } +} diff --git a/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_button.dart b/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_button.dart index 53d269738..cddb86cea 100644 --- a/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_button.dart +++ b/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_button.dart @@ -1,15 +1,24 @@ +import 'dart:developer'; + import 'package:at_contacts_flutter/at_contacts_flutter.dart'; import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; +import 'package:at_onboarding_flutter/screen/at_onboarding_home_screen.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:npt_flutter/constants.dart'; import 'package:npt_flutter/features/onboarding/onboarding.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; +import 'package:npt_flutter/features/onboarding/widgets/onboarding_dialog.dart'; +import 'package:npt_flutter/routes.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:phosphor_flutter/phosphor_flutter.dart'; -Future loadAtClientPreference() async { +Future loadAtClientPreference(String rootDomain) async { var dir = await getApplicationSupportDirectory(); return AtClientPreference() - ..rootDomain = Constants.rootDomain + ..rootDomain = rootDomain ..namespace = Constants.namespace ..hiveStoragePath = dir.path ..commitLogPath = dir.path @@ -17,55 +26,85 @@ Future loadAtClientPreference() async { } class OnboardingButton extends StatefulWidget { - const OnboardingButton({super.key, required this.nextRoute}); - final String nextRoute; + const OnboardingButton({ + super.key, + }); @override State createState() => _OnboardingButtonState(); } class _OnboardingButtonState extends State { - final Future futurePreference = loadAtClientPreference(); - - @override - void initState() { - super.initState(); - onboard(isFromInitState: true); - } - - @override - Widget build(BuildContext context) { - return ElevatedButton( - onPressed: onboard, - child: const Text('Login'), + Future onboard({String? atsign, required String rootDomain, bool isFromInitState = false}) async { + var atSigns = await KeyChainManager.getInstance().getAtSignListFromKeychain(); + var config = AtOnboardingConfig( + atClientPreference: await loadAtClientPreference(rootDomain), + rootEnvironment: RootEnvironment.Production, + domain: rootDomain, + appAPIKey: Constants.appAPIKey, ); - } - Future onboard({bool isFromInitState = false}) async { - AtOnboardingResult onboardingResult = await AtOnboarding.onboard( - // ignore: use_build_context_synchronously - context: context, - config: AtOnboardingConfig( - atClientPreference: await futurePreference, - rootEnvironment: RootEnvironment.Testing, - domain: Constants.rootDomain, - appAPIKey: Constants.appAPIKey, - ), - ); + AtOnboardingResult? onboardingResult; + if (!atSigns.contains(atsign)) { + // This is a hack. + // Ideally it should be possible to skip the home screen in onboarding + // and go straight to either of the following (based on current atSign status): + // A) opening the file picker + // B) activating the atSign + // But unfortunately that code is SO coupled to the widget that it is really + // not worth the effort to fix right now. + // + // Assumptions made in at_onboarding_flutter which have caused this problem: + // if [atsign] is non-null the atSign already exists in the keychain + // thus any atsign that isn't in the keychain isn't handled if explicitly passed... + // this means there is no edge case handling for new or unactivated atSigns + // nor for atSigns that are activated but not in the keychain... + // + // Given that we are working on new user flows, I'm not going to waste countless + // hours for this tiny UX fix + + // TODO: fix localizations + await AtOnboardingLocalizations.load(const Locale("en")); + onboardingResult = await Navigator.push( + // ignore: use_build_context_synchronously + context, + MaterialPageRoute( + builder: (BuildContext context) { + return AtOnboardingHomeScreen( + config: config, + isFromIntroScreen: false, + ); + }, + ), + ); + } else { + onboardingResult = await AtOnboarding.onboard( + atsign: atsign, + // ignore: use_build_context_synchronously + context: context, + config: config, + ); + } if (mounted) { - switch (onboardingResult.status) { + switch (onboardingResult?.status ?? AtOnboardingResultStatus.cancel) { case AtOnboardingResultStatus.success: - await initializeContactsService(rootDomain: Constants.rootDomain); - postOnboard(onboardingResult.atsign!); - Navigator.of(context).pushReplacementNamed(widget.nextRoute); + await initializeContactsService(rootDomain: rootDomain); + postOnboard(onboardingResult!.atsign!, rootDomain); + final result = + await saveAtsignInformation(AtsignInformation(atSign: onboardingResult.atsign!, rootDomain: rootDomain)); + log('atsign result is:$result'); + + if (mounted) { + Navigator.of(context).pushReplacementNamed(Routes.dashboard); + } break; case AtOnboardingResultStatus.error: if (isFromInitState) break; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( backgroundColor: Colors.red, - content: Text('An error has occurred'), + content: Text(AppLocalizations.of(context)!.onboardingError), ), ); break; @@ -74,4 +113,51 @@ class _OnboardingButtonState extends State { } } } + + Future selectAtsign() async { + var options = await getAtsignEntries(); + if (mounted) { + final cubit = context.read(); + String atsign = cubit.state.atSign; + String? rootDomain = cubit.state.rootDomain; + + if (options.isEmpty) { + atsign = ""; + } else if (atsign.isEmpty) { + atsign = options.keys.first; + } + if (options.keys.contains(atsign)) { + rootDomain = options[atsign]?.rootDomain; + } else { + rootDomain = Constants.getRootDomains(context).keys.first; + } + + cubit.setState(atSign: atsign, rootDomain: rootDomain); + final results = await showDialog( + context: context, + builder: (BuildContext context) => OnboardingDialog(options: options), + ); + return results ?? false; + } + return false; + } + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return ElevatedButton.icon( + onPressed: () async { + bool shouldOnboard = await selectAtsign(); + if (shouldOnboard && context.mounted) { + var atsignInformation = context.read().state; + onboard(atsign: atsignInformation.atSign, rootDomain: atsignInformation.rootDomain); + } + }, + icon: PhosphorIcon(PhosphorIcons.arrowUpRight()), + label: Text( + strings.getStarted, + ), + iconAlignment: IconAlignment.end, + ); + } } diff --git a/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_dialog.dart b/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_dialog.dart new file mode 100644 index 000000000..ebbfd857f --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/onboarding/widgets/onboarding_dialog.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; +import 'package:npt_flutter/features/onboarding/widgets/at_directory_selector.dart'; +import 'package:npt_flutter/features/onboarding/widgets/atsign_selector.dart'; +import 'package:npt_flutter/styles/sizes.dart'; +import 'package:npt_flutter/widgets/custom_container.dart'; + +class OnboardingDialog extends StatelessWidget { + const OnboardingDialog({required this.options, super.key}); + final Map options; + + @override + Widget build(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return AlertDialog( + backgroundColor: Colors.white, + content: Padding( + padding: const EdgeInsets.symmetric(vertical: Sizes.p12, horizontal: Sizes.p16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomContainer.background( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Select or type the client atSign"), + gapH16, + AtsignSelector( + options: options, + ), + gapH16, + const Text("Select or type the root domain"), + AtDirectorySelector( + options: options, + ), + ], + ), + ), + gapH10, + CustomContainer.background( + child: Row( + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(strings.cancel), + ), + const Spacer(), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text("Next"), + ), + ], + )) + ], + ), + ), + ); + } +} diff --git a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart index 480a1e453..4c1167bab 100644 --- a/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart +++ b/packages/dart/npt_flutter/lib/features/profile/bloc/profile_bloc.dart @@ -149,6 +149,7 @@ class ProfileBloc extends LoggingBloc { atClient: atClient, params: profile.toNptParams( clientAtsign: atSign, + rootDomain: atClient.getPreferences()!.rootDomain, fallbackRelayAtsign: settings.relayAtsign, overrideRelayWithFallback: settings.overrideRelay, ), diff --git a/packages/dart/npt_flutter/lib/features/profile/cubit/profile_cache_cubit.dart b/packages/dart/npt_flutter/lib/features/profile/cubit/profile_cache_cubit.dart index 3420ce22f..3f620453a 100644 --- a/packages/dart/npt_flutter/lib/features/profile/cubit/profile_cache_cubit.dart +++ b/packages/dart/npt_flutter/lib/features/profile/cubit/profile_cache_cubit.dart @@ -16,4 +16,6 @@ class ProfileCacheCubit extends LoggingCubit { emit(state.withAdded(uuid, bloc)); return bloc; } + + void clear() => emit(const ProfileCacheState({})); } diff --git a/packages/dart/npt_flutter/lib/features/profile/models/profile.dart b/packages/dart/npt_flutter/lib/features/profile/models/profile.dart index 0bf929839..73798499c 100644 --- a/packages/dart/npt_flutter/lib/features/profile/models/profile.dart +++ b/packages/dart/npt_flutter/lib/features/profile/models/profile.dart @@ -1,6 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:noports_core/npt.dart'; -import 'package:npt_flutter/constants.dart'; import 'package:npt_flutter/app.dart'; import 'package:npt_flutter/features/favorite/favorite.dart'; import 'package:npt_flutter/util/uuid.dart'; @@ -87,13 +86,12 @@ final class Profile extends Loggable with Favoritable { NptParams toNptParams({ required String clientAtsign, + required String rootDomain, required String fallbackRelayAtsign, bool overrideRelayWithFallback = false, }) { String srvdAtSign = fallbackRelayAtsign; - if (!overrideRelayWithFallback && - relayAtsign != null && - relayAtsign!.isNotEmpty) { + if (!overrideRelayWithFallback && relayAtsign != null && relayAtsign!.isNotEmpty) { srvdAtSign = relayAtsign!; } return NptParams( @@ -104,7 +102,7 @@ final class Profile extends Loggable with Favoritable { remotePort: remotePort, device: deviceName, localPort: localPort, - rootDomain: Constants.rootDomain, + rootDomain: rootDomain, // hardcoded for now, because it makes the app simpler // and there's very few use-cases where you wouldn't want these settings diff --git a/packages/dart/npt_flutter/lib/features/profile/view/profile_header_view.dart b/packages/dart/npt_flutter/lib/features/profile/view/profile_header_view.dart index 7100478c2..75efd1e6f 100644 --- a/packages/dart/npt_flutter/lib/features/profile/view/profile_header_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile/view/profile_header_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:npt_flutter/features/profile/widgets/profile_header_column.dart'; import 'package:npt_flutter/features/profile_list/profile_list.dart'; import 'package:npt_flutter/features/settings/settings.dart'; import 'package:npt_flutter/styles/sizes.dart'; @@ -14,6 +15,7 @@ class ProfileHeaderView extends StatelessWidget { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; + return BlocBuilder(builder: (context, state) { if (state is ProfileListInitial) { context.read().add(const ProfileListLoadEvent()); @@ -50,12 +52,12 @@ class ProfileHeaderView extends StatelessWidget { null => const Center(child: Spinner()), PreferredViewLayout.minimal => CustomCard.profileHeader( child: Padding( - padding: const EdgeInsets.all(Sizes.p10), + padding: const EdgeInsets.symmetric(vertical: Sizes.p10), child: Row( children: [ const ProfileSelectAllBox(), gapW10, - SizedBox(width: Sizes.p150, child: Text(strings.profileName)), + ProfileHeaderColumn(title: strings.profileName, layout: PreferredViewLayout.minimal), gapW10, Text(strings.status), // gapW10, @@ -74,11 +76,11 @@ class ProfileHeaderView extends StatelessWidget { children: [ const ProfileSelectAllBox(), gapW10, - SizedBox(width: Sizes.p150, child: Text(strings.profileName)), + ProfileHeaderColumn(title: strings.profileName), gapW10, - SizedBox(width: Sizes.p150, child: Text(strings.deviceName)), + ProfileHeaderColumn(title: strings.deviceName), gapW10, - SizedBox(width: Sizes.p150, child: Text(strings.serviceMapping)), + ProfileHeaderColumn(title: strings.serviceMapping), gapW10, Text(strings.status), // gapW10, diff --git a/packages/dart/npt_flutter/lib/features/profile/view/profile_view.dart b/packages/dart/npt_flutter/lib/features/profile/view/profile_view.dart index 9d8e829ca..16b47cda0 100644 --- a/packages/dart/npt_flutter/lib/features/profile/view/profile_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile/view/profile_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:npt_flutter/features/profile/profile.dart'; import 'package:npt_flutter/features/settings/settings.dart'; +import 'package:npt_flutter/styles/sizes.dart'; import 'package:npt_flutter/widgets/loader_bar.dart'; import 'package:npt_flutter/widgets/spinner.dart'; @@ -21,6 +22,7 @@ class ProfileView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ LoaderBar(), + gapW10, ProfileRefreshButton(), ], ); diff --git a/packages/dart/npt_flutter/lib/features/profile/view/profile_view_minimal.dart b/packages/dart/npt_flutter/lib/features/profile/view/profile_view_minimal.dart index ec4c8eb73..e147f682f 100644 --- a/packages/dart/npt_flutter/lib/features/profile/view/profile_view_minimal.dart +++ b/packages/dart/npt_flutter/lib/features/profile/view/profile_view_minimal.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:npt_flutter/features/profile/profile.dart'; +import 'package:npt_flutter/features/settings/models/settings.dart'; import 'package:npt_flutter/styles/sizes.dart'; class ProfileViewMinimal extends StatelessWidget { @@ -10,11 +11,10 @@ class ProfileViewMinimal extends StatelessWidget { return const Row(children: [ ProfileSelectBox(), gapW10, - ProfileDisplayName(), + ProfileDisplayName(layout: PreferredViewLayout.minimal), gapW10, ProfileStatusIndicator(), gapW10, - Spacer(), ProfileRunButton(), gapW10, ProfileFavoriteButton(), diff --git a/packages/dart/npt_flutter/lib/features/profile/view/profile_view_ssh_style.dart b/packages/dart/npt_flutter/lib/features/profile/view/profile_view_ssh_style.dart index a32bed57f..88689560f 100644 --- a/packages/dart/npt_flutter/lib/features/profile/view/profile_view_ssh_style.dart +++ b/packages/dart/npt_flutter/lib/features/profile/view/profile_view_ssh_style.dart @@ -18,7 +18,6 @@ class ProfileViewSshStyle extends StatelessWidget { gapW10, ProfileStatusIndicator(), gapW10, - Spacer(), ProfileRunButton(), gapW10, ProfileFavoriteButton(), diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_device_name.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_device_name.dart index 30c1f3700..0d898bdcb 100644 --- a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_device_name.dart +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_device_name.dart @@ -9,16 +9,20 @@ class ProfileDeviceName extends StatelessWidget { @override Widget build(BuildContext context) { + final deviceWidth = MediaQuery.of(context).size.width; return SizedBox( - width: Sizes.p150, - child: BlocSelector(selector: (state) { - if (state is! ProfileLoadedState) return null; - return (state.profile.deviceName, state.profile.sshnpdAtsign); - }, builder: (BuildContext context, (String, String)? tuple) { - if (tuple == null) return gap0; - var (deviceName, sshnpdAtSign) = tuple; - return Text('$deviceName$sshnpdAtSign'); - }), + width: deviceWidth * Sizes.profileFieldsWidthFactor, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: BlocSelector(selector: (state) { + if (state is! ProfileLoadedState) return null; + return (state.profile.deviceName, state.profile.sshnpdAtsign); + }, builder: (BuildContext context, (String, String)? tuple) { + if (tuple == null) return gap0; + var (deviceName, sshnpdAtSign) = tuple; + return Text('$deviceName$sshnpdAtSign'); + }), + ), ); } } diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_display_name.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_display_name.dart index a04292431..79c35a3ea 100644 --- a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_display_name.dart +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_display_name.dart @@ -1,27 +1,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:npt_flutter/features/profile/profile.dart'; +import 'package:npt_flutter/features/settings/models/settings.dart'; import '../../../styles/sizes.dart'; class ProfileDisplayName extends StatelessWidget { - const ProfileDisplayName({super.key}); + const ProfileDisplayName({super.key, this.layout = PreferredViewLayout.sshStyle}); + + final PreferredViewLayout layout; @override Widget build(BuildContext context) { + final deviceWidth = MediaQuery.of(context).size.width; + final double widthFactor = + layout == PreferredViewLayout.sshStyle ? Sizes.profileFieldsWidthFactor : Sizes.profileFieldsWidthFactorAlt; return SizedBox( - width: Sizes.p150, - child: BlocSelector( - selector: (ProfileState state) { - if (state is ProfileLoadedState) { - return state.profile.displayName; - } - return null; - }, - builder: (BuildContext context, String? displayName) { - if (displayName == null) return gap0; - return Text(displayName); - }, + width: deviceWidth * widthFactor, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: BlocSelector( + selector: (ProfileState state) { + if (state is ProfileLoadedState) { + return state.profile.displayName; + } + return null; + }, + builder: (BuildContext context, String? displayName) { + if (displayName == null) return gap0; + return Text(displayName); + }, + ), ), ); } diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_header_column.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_header_column.dart new file mode 100644 index 000000000..e00f1882b --- /dev/null +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_header_column.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:npt_flutter/features/settings/models/settings.dart'; +import 'package:npt_flutter/styles/sizes.dart'; + +class ProfileHeaderColumn extends StatelessWidget { + const ProfileHeaderColumn({super.key, required this.title, this.layout = PreferredViewLayout.sshStyle}); + + final String title; + final PreferredViewLayout layout; + + @override + Widget build(BuildContext context) { + final deviceWidth = MediaQuery.of(context).size.width; + final double widthFactor = + layout == PreferredViewLayout.sshStyle ? Sizes.profileFieldsWidthFactor : Sizes.profileFieldsWidthFactorAlt; + + return SizedBox(width: deviceWidth * widthFactor, child: Text(title)); + } +} diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_service_view.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_service_view.dart index 34f4f9803..51da54c86 100644 --- a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_service_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_service_view.dart @@ -9,16 +9,20 @@ class ProfileServiceView extends StatelessWidget { @override Widget build(BuildContext context) { + final deviceWidth = MediaQuery.of(context).size.width; return SizedBox( - width: Sizes.p150, - child: BlocSelector(selector: (state) { - if (state is! ProfileLoadedState) return null; - return (state.profile.localPort, state.profile.remoteHost, state.profile.remotePort); - }, builder: (BuildContext context, (int, String, int)? triple) { - if (triple == null) return gap0; - var (localPort, remoteHost, remotePort) = triple; - return Text('$localPort:$remoteHost:$remotePort'); - }), + width: deviceWidth * Sizes.profileFieldsWidthFactor, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: BlocSelector(selector: (state) { + if (state is! ProfileLoadedState) return null; + return (state.profile.localPort, state.profile.remoteHost, state.profile.remotePort); + }, builder: (BuildContext context, (int, String, int)? triple) { + if (triple == null) return gap0; + var (localPort, remoteHost, remotePort) = triple; + return Text('$localPort:$remoteHost:$remotePort'); + }), + ), ); } } diff --git a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_status_indicator.dart b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_status_indicator.dart index 1e1085625..9bf2c41b7 100644 --- a/packages/dart/npt_flutter/lib/features/profile/widgets/profile_status_indicator.dart +++ b/packages/dart/npt_flutter/lib/features/profile/widgets/profile_status_indicator.dart @@ -8,23 +8,25 @@ class ProfileStatusIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - width: Sizes.p150, - child: BlocBuilder(builder: (BuildContext context, ProfileState state) { - if (state is ProfileFailedSave) { - return const Tooltip(message: 'error saving profile', child: Text("Failed")); - } + return Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: BlocBuilder(builder: (BuildContext context, ProfileState state) { + if (state is ProfileFailedSave) { + return const Tooltip(message: 'error saving profile', child: Text("Failed")); + } - if (state is ProfileFailedStart) { - return Tooltip(message: state.reason ?? 'No Reason Provided', child: const Text("Failed")); - } + if (state is ProfileFailedStart) { + return Tooltip(message: state.reason ?? 'No Reason Provided', child: const Text("Failed")); + } - if (state is ProfileStarting && state.status != null) { - return Text(state.status!); - } + if (state is ProfileStarting && state.status != null) { + return Text(state.status!); + } - return gap0; - }), + return gap0; + }), + ), ); } } diff --git a/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart b/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart index 7f7ed730f..7f1ede75c 100644 --- a/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile_form/view/profile_form_view.dart @@ -14,21 +14,24 @@ class ProfileFormView extends StatelessWidget { Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; final GlobalKey formkey = GlobalKey(); + final deviceSize = MediaQuery.of(context).size; return BlocProvider( create: (BuildContext context) => /// Local copy of the profile which is used by the form ProfileBloc(context.read(), uuid)..add(const ProfileLoadOrCreateEvent()), child: Padding( - padding: const EdgeInsets.only(left: Sizes.p100, right: Sizes.p100, top: Sizes.p20), + padding: const EdgeInsets.only(left: Sizes.p100, right: Sizes.p100), child: Stack( children: [ Align( alignment: Alignment.topCenter, child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CustomCard.profileFormContent( + height: deviceSize.height * Sizes.dashboardCardHeightFactor, child: SingleChildScrollView( child: Form( key: formkey, @@ -91,7 +94,6 @@ class ProfileFormView extends StatelessWidget { ), ), ), - gapH16, Text(strings.allRightsReserved), ], ), diff --git a/packages/dart/npt_flutter/lib/features/profile_form/widgets/profile_relay_quick_buttons.dart b/packages/dart/npt_flutter/lib/features/profile_form/widgets/profile_relay_quick_buttons.dart index 2e5ffcfdf..753d7f973 100644 --- a/packages/dart/npt_flutter/lib/features/profile_form/widgets/profile_relay_quick_buttons.dart +++ b/packages/dart/npt_flutter/lib/features/profile_form/widgets/profile_relay_quick_buttons.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:npt_flutter/constants.dart'; import 'package:npt_flutter/features/profile/profile.dart'; import 'package:npt_flutter/features/profile_form/widgets/profile_relay_at_sign_text_field.dart'; import 'package:npt_flutter/styles/sizes.dart'; +import 'package:npt_flutter/util/relay.dart'; import 'package:npt_flutter/widgets/custom_container.dart'; class ProfileRelayQuickButtons extends StatelessWidget { @@ -44,29 +44,29 @@ class ProfileRelayQuickButtons extends StatelessWidget { scrollDirection: Axis.horizontal, controller: controller, children: [ - ...Constants.defaultRelayOptions.entries.map( - (e) => Padding( - padding: const EdgeInsets.only(right: Sizes.p10), - child: CustomContainer.foreground( - key: Key(e.key), - child: SizedBox( - width: Sizes.p200, - height: Sizes.p50, - child: RadioListTile( - title: Text(e.value), - value: e.key, - groupValue: relayAtsign, - onChanged: (value) { - var bloc = context.read(); - bloc.add(ProfileEditEvent( - profile: (bloc.state as ProfileLoadedState).profile.copyWith(relayAtsign: value), - )); - }, + ...RelayUtil.getRelayDisplayNameMap(context).entries.map( + (e) => Padding( + padding: const EdgeInsets.only(right: Sizes.p10), + child: CustomContainer.foreground( + key: Key(e.key), + child: SizedBox( + width: Sizes.p200, + height: Sizes.p50, + child: RadioListTile( + title: Text(e.value), + value: e.key, + groupValue: relayAtsign, + onChanged: (value) { + var bloc = context.read(); + bloc.add(ProfileEditEvent( + profile: (bloc.state as ProfileLoadedState).profile.copyWith(relayAtsign: value), + )); + }, + ), + ), ), ), ), - ), - ), const ProfileRelayAtSignTextField(), ], ), diff --git a/packages/dart/npt_flutter/lib/features/profile_list/bloc/profile_list_bloc.dart b/packages/dart/npt_flutter/lib/features/profile_list/bloc/profile_list_bloc.dart index c3ea1178c..4cf5f4c19 100644 --- a/packages/dart/npt_flutter/lib/features/profile_list/bloc/profile_list_bloc.dart +++ b/packages/dart/npt_flutter/lib/features/profile_list/bloc/profile_list_bloc.dart @@ -17,7 +17,10 @@ class ProfileListBloc extends LoggingBloc { on(_onAdd); } - Future _onLoad(ProfileListLoadEvent event, Emitter emit) async { + void clearAll() => emit(const ProfileListInitial()); + + Future _onLoad( + ProfileListLoadEvent event, Emitter emit) async { emit(const ProfileListLoading()); Iterable? profiles; @@ -35,11 +38,13 @@ class ProfileListBloc extends LoggingBloc { emit(ProfileListLoaded(profiles: profiles)); } - Future _onUpdate(ProfileListUpdateEvent event, Emitter emit) async { + Future _onUpdate( + ProfileListUpdateEvent event, Emitter emit) async { emit(ProfileListLoaded(profiles: event.profiles)); } - Future _onDelete(ProfileListDeleteEvent event, Emitter emit) async { + Future _onDelete( + ProfileListDeleteEvent event, Emitter emit) async { // Don't allow deletes unless listed is loaded - this reduces the number of edge cases significantly if (state is! ProfileListLoaded) { return; @@ -64,7 +69,8 @@ class ProfileListBloc extends LoggingBloc { bloc?.add(FavoriteRemoveEvent(favoritesToRemove)); } - Future _onAdd(ProfileListAddEvent event, Emitter emit) async { + Future _onAdd( + ProfileListAddEvent event, Emitter emit) async { // Don't allow async bulk adds unless listed is loaded - this reduces the number of edge cases significantly if (state is! ProfileListLoaded) { return; diff --git a/packages/dart/npt_flutter/lib/features/profile_list/cubit/profiles_running_cubit.dart b/packages/dart/npt_flutter/lib/features/profile_list/cubit/profiles_running_cubit.dart index 64bdb8290..57b5c4da9 100644 --- a/packages/dart/npt_flutter/lib/features/profile_list/cubit/profiles_running_cubit.dart +++ b/packages/dart/npt_flutter/lib/features/profile_list/cubit/profiles_running_cubit.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:npt_flutter/app.dart'; import 'package:socket_connector/socket_connector.dart'; @@ -19,4 +17,11 @@ class ProfilesRunningCubit extends LoggingCubit { void invalidate(String uuid) { emit(state.withoutConnector(uuid)); } + + void stopAllAndClear() { + state.socketConnectors.forEach((_, socketConnector) { + socketConnector?.close(); + }); + emit(const ProfilesRunningState({})); + } } diff --git a/packages/dart/npt_flutter/lib/features/profile_list/view/profile_list_view.dart b/packages/dart/npt_flutter/lib/features/profile_list/view/profile_list_view.dart index bfe02ac40..6c689682a 100644 --- a/packages/dart/npt_flutter/lib/features/profile_list/view/profile_list_view.dart +++ b/packages/dart/npt_flutter/lib/features/profile_list/view/profile_list_view.dart @@ -16,6 +16,9 @@ class ProfileListView extends StatelessWidget { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; + final deviceSize = MediaQuery.of(context).size; + final bodyMedium = Theme.of(context).textTheme.labelSmall; + SizeConfig().init(); return BlocBuilder(builder: (context, state) { return switch (state) { ProfileListInitial() || ProfileListLoading() => const Center(child: Spinner()), @@ -50,8 +53,11 @@ class ProfileListView extends StatelessWidget { alignment: Alignment.topCenter, child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ CustomCard.dashboardContent( + height: deviceSize.height * Sizes.dashboardCardHeightFactor, + width: deviceSize.width * Sizes.dashboardCardWidthFactor, child: Column( children: [ isFullProfile @@ -112,8 +118,10 @@ class ProfileListView extends StatelessWidget { ], ), ), - gapH16, - Text(strings.allRightsReserved) + Text( + strings.allRightsReserved, + style: bodyMedium?.copyWith(fontSize: bodyMedium.fontSize?.toFont), + ), ], ), ), diff --git a/packages/dart/npt_flutter/lib/features/settings/bloc/settings_bloc.dart b/packages/dart/npt_flutter/lib/features/settings/bloc/settings_bloc.dart index 1a073303b..01c28217f 100644 --- a/packages/dart/npt_flutter/lib/features/settings/bloc/settings_bloc.dart +++ b/packages/dart/npt_flutter/lib/features/settings/bloc/settings_bloc.dart @@ -12,6 +12,8 @@ class SettingsBloc extends LoggingBloc { on(_onEdit); } + void clear() => emit(const SettingsInitial()); + Future _onLoad( SettingsLoadEvent event, Emitter emit) async { emit(const SettingsLoading()); diff --git a/packages/dart/npt_flutter/lib/features/settings/repository/contact_repository.dart b/packages/dart/npt_flutter/lib/features/settings/repository/contact_repository.dart index 2d5ca1066..bd2e6d291 100644 --- a/packages/dart/npt_flutter/lib/features/settings/repository/contact_repository.dart +++ b/packages/dart/npt_flutter/lib/features/settings/repository/contact_repository.dart @@ -20,7 +20,6 @@ class ContactsService { final AtSignLogger _logger = AtSignLogger(Constants.namespace!); AtClient? atClient; - AtClientService? atClientService; var atClientManager = AtClientManager.getInstance(); static var atContactService = ContactService(); @@ -31,20 +30,24 @@ class ContactsService { /// Fetch the current atsign profile image Future getCurrentAtsignProfileImage() async { - return atContactService.getContactDetails(atClientManager.atClient.getCurrentAtSign(), null).then((value) { + return atContactService + .getContactDetails(atClientManager.atClient.getCurrentAtSign(), null) + .then((value) { return value['image']; }); } /// Fetch details for the current atsign Future> getCurrentAtsignContactDetails() { - return atContactService.getContactDetails(atClientManager.atClient.getCurrentAtSign(), null); + return atContactService.getContactDetails( + atClientManager.atClient.getCurrentAtSign(), null); } /// Delete contact from contact list. Future addContact(String atSign, String? nickname) async { try { - bool isAdded = await atContactService.addAtSign(atSign: atSign, nickName: nickname); + bool isAdded = + await atContactService.addAtSign(atSign: atSign, nickName: nickname); return isAdded; } on AtClientException catch (atClientExcep) { diff --git a/packages/dart/npt_flutter/lib/features/settings/repository/settings_repository.dart b/packages/dart/npt_flutter/lib/features/settings/repository/settings_repository.dart index 9c6adfd1a..863f97f8e 100644 --- a/packages/dart/npt_flutter/lib/features/settings/repository/settings_repository.dart +++ b/packages/dart/npt_flutter/lib/features/settings/repository/settings_repository.dart @@ -6,11 +6,9 @@ import 'package:npt_flutter/features/settings/settings.dart'; class SettingsRepository { const SettingsRepository(); - AtKey get settingsAtKey => - AtKey.self('settings', namespace: Constants.namespace).build(); + AtKey get settingsAtKey => AtKey.self('settings', namespace: Constants.namespace).build(); - Settings get defaultSettings => Settings( - relayAtsign: Constants.defaultRelayOptions.values.first, + Settings get defaultSettings => const Settings( viewLayout: PreferredViewLayout.minimal, overrideRelay: false, ); @@ -18,8 +16,7 @@ class SettingsRepository { Future getSettings() async { AtClient atClient = AtClientManager.getInstance().atClient; try { - var value = await atClient - .get(settingsAtKey..sharedBy = atClient.getCurrentAtSign()); + var value = await atClient.get(settingsAtKey..sharedBy = atClient.getCurrentAtSign()); if (value.value == null) { // No settings saved, so use the defaults return defaultSettings; @@ -43,8 +40,7 @@ class SettingsRepository { Future deleteSettings(Settings settings) async { AtClient atClient = AtClientManager.getInstance().atClient; try { - return await atClient - .delete(settingsAtKey..sharedBy = atClient.getCurrentAtSign()); + return await atClient.delete(settingsAtKey..sharedBy = atClient.getCurrentAtSign()); } catch (_) { return false; } diff --git a/packages/dart/npt_flutter/lib/features/settings/view/settings_view.dart b/packages/dart/npt_flutter/lib/features/settings/view/settings_view.dart index a43939245..c1b63347f 100644 --- a/packages/dart/npt_flutter/lib/features/settings/view/settings_view.dart +++ b/packages/dart/npt_flutter/lib/features/settings/view/settings_view.dart @@ -19,6 +19,7 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; + final deviceSize = MediaQuery.of(context).size; return BlocBuilder( builder: (context, state) { if (state is SettingsInitial) { @@ -29,14 +30,34 @@ class SettingsView extends StatelessWidget { case SettingsLoading(): return const Center(child: Spinner()); case SettingsLoadedState(): - return Padding( - padding: const EdgeInsets.only(top: 18, bottom: 92, left: 120, right: 77), - child: Stack( - clipBehavior: Clip.none, - children: [ - Positioned( - left: Sizes.p192, - child: CustomCard.settingsContent( + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomCard.settingsRail( + height: deviceSize.height * Sizes.settingsCardHeightFactor, + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // gapH30, + CustomTextButton.discord(), + CustomTextButton.email(), + CustomTextButton.faq(), + CustomTextButton.privacyPolicy(), + CustomTextButton.feedback(), + CustomTextButton.backUpYourKey(), + CustomTextButton.signOut(), + ContactListTile(), + ], + ), + ), + CustomCard.settingsContent( + height: deviceSize.height * Sizes.settingsCardHeightFactor, + width: deviceSize.width * Sizes.settingsCardWidthFactor, child: Padding( padding: const EdgeInsets.only( left: Sizes.p43, @@ -55,35 +76,10 @@ class SettingsView extends StatelessWidget { ]), ), ), - ), - const Positioned( - left: 0, - child: CustomCard.settingsRail( - child: Padding( - padding: EdgeInsets.all(0.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - gapH30, - CustomTextButton.discord(), - CustomTextButton.email(), - CustomTextButton.faq(), - CustomTextButton.privacyPolicy(), - CustomTextButton.feedback(), - CustomTextButton.backUpYourKey(), - CustomTextButton.resetAtsign(), - ContactListTile(), - ], - ), - ), - ), - ), - Positioned( - top: Sizes.p470, - child: Text(strings.allRightsReserved), - ), - ], - ), + ], + ), + Text(strings.allRightsReserved) + ], ); } }, diff --git a/packages/dart/npt_flutter/lib/features/settings/widgets/contact_list_tile.dart b/packages/dart/npt_flutter/lib/features/settings/widgets/contact_list_tile.dart index 336eba6b2..10b0255a2 100644 --- a/packages/dart/npt_flutter/lib/features/settings/widgets/contact_list_tile.dart +++ b/packages/dart/npt_flutter/lib/features/settings/widgets/contact_list_tile.dart @@ -9,7 +9,7 @@ class ContactListTile extends StatelessWidget { @override Widget build(BuildContext context) { - SizeConfig().init(context); + SizeConfig().init(); final contactRepo = ContactsService.getInstance(); final bodyMedium = Theme.of(context).textTheme.bodyMedium!; diff --git a/packages/dart/npt_flutter/lib/features/settings/widgets/settings_dashboard_layout_selector.dart b/packages/dart/npt_flutter/lib/features/settings/widgets/settings_dashboard_layout_selector.dart index d1b4680dd..0adc07e15 100644 --- a/packages/dart/npt_flutter/lib/features/settings/widgets/settings_dashboard_layout_selector.dart +++ b/packages/dart/npt_flutter/lib/features/settings/widgets/settings_dashboard_layout_selector.dart @@ -46,25 +46,29 @@ class SettingsDashboardLayoutSelector extends StatelessWidget { ], ), gapH18, - CustomCard.settingsPreview( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - gapH13, - Padding( - padding: const EdgeInsets.only(left: Sizes.p20), - child: Align( - alignment: Alignment.centerLeft, - child: Text(strings.preview), + SizedBox( + height: 295, + width: 537, + child: CustomCard.settingsPreview( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + gapH13, + Padding( + padding: const EdgeInsets.only(left: Sizes.p20), + child: Align( + alignment: Alignment.centerLeft, + child: Text(strings.preview), + ), ), - ), - gapH10, - viewLayout == PreferredViewLayout.minimal - ? SvgPicture.asset('assets/simple.svg') - : SvgPicture.asset('assets/advance.svg'), - gapH16, - ], - )) + gapH10, + viewLayout == PreferredViewLayout.minimal + ? SvgPicture.asset('assets/simple.svg') + : SvgPicture.asset('assets/advance.svg'), + gapH16, + ], + )), + ) ], ); }); diff --git a/packages/dart/npt_flutter/lib/features/settings/widgets/settings_relay_quick_buttons.dart b/packages/dart/npt_flutter/lib/features/settings/widgets/settings_relay_quick_buttons.dart index 7c2b57bab..1888bfb47 100644 --- a/packages/dart/npt_flutter/lib/features/settings/widgets/settings_relay_quick_buttons.dart +++ b/packages/dart/npt_flutter/lib/features/settings/widgets/settings_relay_quick_buttons.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:npt_flutter/constants.dart'; import 'package:npt_flutter/features/settings/settings.dart'; +import 'package:npt_flutter/util/relay.dart'; import 'package:npt_flutter/widgets/custom_container.dart'; import '../../../styles/sizes.dart'; @@ -29,29 +29,29 @@ class SettingsRelayQuickButtons extends StatelessWidget { controller: controller, scrollDirection: Axis.horizontal, children: [ - ...Constants.defaultRelayOptions.entries.map( - (e) => Padding( - padding: const EdgeInsets.only(right: Sizes.p10), - child: CustomContainer.foreground( - key: Key(e.key), - child: SizedBox( - width: Sizes.p180, - child: RadioListTile( - title: Text(e.value), - value: e.key, - groupValue: relayAtsign, - onChanged: (value) { - var bloc = context.read(); - bloc.add(SettingsEditEvent( - settings: (bloc.state as SettingsLoadedState).settings.copyWith(relayAtsign: value), - save: true, - )); - }, + ...RelayUtil.getRelayDisplayNameMap(context).entries.map( + (e) => Padding( + padding: const EdgeInsets.only(right: Sizes.p10), + child: CustomContainer.foreground( + key: Key(e.key), + child: SizedBox( + width: Sizes.p180, + child: RadioListTile( + title: Text(e.value), + value: e.key, + groupValue: relayAtsign, + onChanged: (value) { + var bloc = context.read(); + bloc.add(SettingsEditEvent( + settings: (bloc.state as SettingsLoadedState).settings.copyWith(relayAtsign: value), + save: true, + )); + }, + ), + ), ), ), ), - ), - ), const SettingsRelayAtSignTextField(), ], ), diff --git a/packages/dart/npt_flutter/lib/features/tray_manager/cubit/tray_cubit.dart b/packages/dart/npt_flutter/lib/features/tray_manager/cubit/tray_cubit.dart index ef2f751c5..3ed3c2beb 100644 --- a/packages/dart/npt_flutter/lib/features/tray_manager/cubit/tray_cubit.dart +++ b/packages/dart/npt_flutter/lib/features/tray_manager/cubit/tray_cubit.dart @@ -25,7 +25,7 @@ part 'tray_state.dart'; if (context == null) return; if (context.mounted) { var cubit = context.read(); - if (cubit.state is! Onboarded) return; + if (cubit.getStatus() != OnboardingStatus.onboarded) return; Navigator.of(context).pushNamedAndRemoveUntil( Routes.settings, (route) => route.isFirst, @@ -70,7 +70,7 @@ class TrayCubit extends LoggingCubit { if (state is! TrayInitial) return; var context = App.navState.currentContext; if (context == null) return; - var showSettings = context.read().state is Onboarded; + var showSettings = context.read().getStatus() == OnboardingStatus.onboarded; await reloadIcon(); @@ -98,7 +98,7 @@ class TrayCubit extends LoggingCubit { var init = initialize(); /// Access the context before any awaited function calls - var showSettings = context.read().state is Onboarded; + var showSettings = context.read().getStatus() == OnboardingStatus.onboarded; var favoriteBloc = context.read(); var profilesList = context.read(); diff --git a/packages/dart/npt_flutter/lib/localization/app_en.arb b/packages/dart/npt_flutter/lib/localization/app_en.arb index 1bb1406b5..0ca210b3e 100644 --- a/packages/dart/npt_flutter/lib/localization/app_en.arb +++ b/packages/dart/npt_flutter/lib/localization/app_en.arb @@ -53,6 +53,9 @@ "remotePort" : "Remote Port", "remotePortDescription" : "", "resetAtsign" : "Reset Atsign", + "rvAmDisplayName" : "Americas", + "rvApDisplayName" : "Asia-Pacific", + "rvEuDisplayName" : "Europe", "selectExportFile": "Please select a file to export to:", "serviceMapping" : "Service Mapping", "settings" : "Settings", @@ -68,5 +71,12 @@ "validationErrorRemoteHostField" : "Field must be partially or fully qualified hostname or an IP address", "profileRunningActionDeniedMessage" : "Cannot perform this action while profile is running", "emptyProfileMessage" : "No profiles found\nCreate or Import a profile to start using NoPorts.", + "onboardingTitle" : "Welcome", + "onboardingSubTitle" : "to NoPorts Desktop", + "getStarted" : "Get Started", + "atDirectory" : "AtDirectory", + "atDirectorySubtitle" : "Select the domain you want to use", + "onboard" : "Onboard", + "onboardingError" : "An error has occurred", "yaml" : "YAML" -} \ No newline at end of file +} diff --git a/packages/dart/npt_flutter/lib/main.dart b/packages/dart/npt_flutter/lib/main.dart index e74895cc1..a4dfdd900 100644 --- a/packages/dart/npt_flutter/lib/main.dart +++ b/packages/dart/npt_flutter/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:window_manager/window_manager.dart'; @@ -8,11 +6,10 @@ import 'app.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); windowManager.ensureInitialized(); - try { - await windowManager.setSkipTaskbar(true); // Don't show the app icon in dock - } catch (_) { - log("Failed to setSkipTaskbar"); - } finally { - runApp(const App()); - } + var windowOptions = const WindowOptions( + title: "NoPorts Desktop", + skipTaskbar: true, + ); + windowManager.waitUntilReadyToShow(windowOptions); + runApp(const App()); } diff --git a/packages/dart/npt_flutter/lib/pages/dashboard_page.dart b/packages/dart/npt_flutter/lib/pages/dashboard_page.dart index 8b6fe312f..86866072b 100644 --- a/packages/dart/npt_flutter/lib/pages/dashboard_page.dart +++ b/packages/dart/npt_flutter/lib/pages/dashboard_page.dart @@ -9,6 +9,7 @@ class DashboardPage extends StatelessWidget { @override Widget build(BuildContext context) { final strings = AppLocalizations.of(context)!; + return Scaffold( appBar: NptAppBar( title: strings.dashboard, diff --git a/packages/dart/npt_flutter/lib/pages/loading_page.dart b/packages/dart/npt_flutter/lib/pages/loading_page.dart new file mode 100644 index 000000000..b26038519 --- /dev/null +++ b/packages/dart/npt_flutter/lib/pages/loading_page.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:npt_flutter/widgets/npt_app_bar.dart'; + +class LoadingPage extends StatelessWidget { + const LoadingPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + extendBodyBehindAppBar: true, + extendBody: true, + appBar: NptAppBar( + isNavigateBack: false, + showSettings: false, + ), + body: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + } +} diff --git a/packages/dart/npt_flutter/lib/pages/onboarding_page.dart b/packages/dart/npt_flutter/lib/pages/onboarding_page.dart index 0e24abfde..4c55a08a8 100644 --- a/packages/dart/npt_flutter/lib/pages/onboarding_page.dart +++ b/packages/dart/npt_flutter/lib/pages/onboarding_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:npt_flutter/features/onboarding/onboarding.dart'; +import 'package:npt_flutter/features/onboarding/view/onboarding_view.dart'; +import 'package:npt_flutter/widgets/npt_app_bar.dart'; class OnboardingPage extends StatelessWidget { const OnboardingPage({super.key, required this.nextRoute}); @@ -7,6 +8,14 @@ class OnboardingPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold(body: OnboardingButton(nextRoute: nextRoute)); + return const Scaffold( + extendBodyBehindAppBar: true, + extendBody: true, + appBar: NptAppBar( + isNavigateBack: false, + showSettings: false, + ), + body: OnboardingView(), + ); } } diff --git a/packages/dart/npt_flutter/lib/pages/pages.dart b/packages/dart/npt_flutter/lib/pages/pages.dart index c5290f584..9a42a3181 100644 --- a/packages/dart/npt_flutter/lib/pages/pages.dart +++ b/packages/dart/npt_flutter/lib/pages/pages.dart @@ -1,4 +1,5 @@ export 'dashboard_page.dart'; +export 'loading_page.dart'; export 'onboarding_page.dart'; export 'profile_form_page.dart'; export 'settings_page.dart'; diff --git a/packages/dart/npt_flutter/lib/routes.dart b/packages/dart/npt_flutter/lib/routes.dart index ba5db84ab..765c46c68 100644 --- a/packages/dart/npt_flutter/lib/routes.dart +++ b/packages/dart/npt_flutter/lib/routes.dart @@ -7,11 +7,13 @@ class Routes { static const dashboard = '/dashboard'; static const settings = '/settings'; static const profileForm = '/profile'; + static const loadingPage = '/loading'; static final Map routes = { onboarding: (_) => const OnboardingPage(nextRoute: dashboard), dashboard: (_) => const DashboardPage(), settings: (_) => const SettingsPage(), profileForm: (_) => const ProfileFormPage(), + loadingPage: (_) => const LoadingPage(), }; } diff --git a/packages/dart/npt_flutter/lib/styles/app_theme.dart b/packages/dart/npt_flutter/lib/styles/app_theme.dart index 01d8210eb..65d0c7a2d 100644 --- a/packages/dart/npt_flutter/lib/styles/app_theme.dart +++ b/packages/dart/npt_flutter/lib/styles/app_theme.dart @@ -4,6 +4,14 @@ import 'package:npt_flutter/styles/sizes.dart'; class AppTheme { static TextTheme lightTextTheme = const TextTheme( + headlineLarge: TextStyle( + fontSize: Sizes.p32, + fontWeight: FontWeight.w600, + ), + headlineMedium: TextStyle( + fontSize: Sizes.p24, + fontWeight: FontWeight.w500, + ), titleMedium: TextStyle( fontSize: Sizes.p18, fontWeight: FontWeight.w600, diff --git a/packages/dart/npt_flutter/lib/styles/sizes.dart b/packages/dart/npt_flutter/lib/styles/sizes.dart index 20c681dcc..9ab9e4bb4 100644 --- a/packages/dart/npt_flutter/lib/styles/sizes.dart +++ b/packages/dart/npt_flutter/lib/styles/sizes.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:npt_flutter/app.dart'; /// Constant sizes to be used in the app (paddings, gaps, rounded corners etc.) class Sizes { @@ -18,7 +19,7 @@ class Sizes { static const p20 = 20.0; // static const p21 = 21.0; // static const p28 = 28.0; - // static const p24 = 24.0; + static const p24 = 24.0; static const p25 = 25.0; static const p27 = 27.0; static const p28 = 28.0; @@ -35,6 +36,7 @@ class Sizes { static const p43 = 43.0; static const p50 = 50.0; static const p54 = 54.0; + static const p70 = 70.0; static const p80 = 80.0; // static const p99 = 99.0; @@ -60,6 +62,13 @@ class Sizes { static const p654 = 654.0; static const p664 = 664.0; static const p941 = 941.0; + // The below size factors are constants that are used to determine the height or width based on the device size. + static const dashboardCardHeightFactor = 489 / 691; + static const dashboardCardWidthFactor = 941 / 1053; + static const profileFieldsWidthFactor = 150 / 1053; + static const profileFieldsWidthFactorAlt = 300 / 1053; + static const settingsCardWidthFactor = 654 / 1053; + static const settingsCardHeightFactor = 438 / 691; } const gap0 = SizedBox(); @@ -94,7 +103,7 @@ const gapH30 = SizedBox(height: Sizes.p30); // const gapH36 = SizedBox(height: Sizes.p36); const gapH40 = SizedBox(height: Sizes.p40); // const gapH46 = SizedBox(height: Sizes.p46); -// const gapH60 = SizedBox(height: Sizes.p60); + const gapH108 = SizedBox(height: Sizes.p108); const kWindowsMinWindowSize = Size(684, 541); @@ -125,14 +134,17 @@ class SizeConfig { double textFactor = 1.0; - bool isMobile(BuildContext context) => MediaQuery.of(context).size.width < 700; + bool isMobile(BuildContext context) => + MediaQuery.of(context).size.width < 700; bool isTablet(BuildContext context) => - MediaQuery.of(context).size.width >= 700 && MediaQuery.of(context).size.width < 1200; - bool isDesktop(BuildContext context) => MediaQuery.of(context).size.width >= 1200; + MediaQuery.of(context).size.width >= 700 && + MediaQuery.of(context).size.width < 1200; + bool isDesktop(BuildContext context) => + MediaQuery.of(context).size.width >= 1200; - void init(BuildContext context) { - _mediaQueryData = MediaQuery.of(context); + void init() { + _mediaQueryData = MediaQuery.of(App.navState.currentContext!); screenWidth = _mediaQueryData.size.width; screenHeight = _mediaQueryData.size.height; refHeight = 505; @@ -146,16 +158,20 @@ class SizeConfig { blockSizeHorizontal = screenWidth / 100; blockSizeVertical = screenHeight / 100; - _safeAreaHorizontal = _mediaQueryData.padding.left + _mediaQueryData.padding.right; - _safeAreaVertical = _mediaQueryData.padding.top + _mediaQueryData.padding.bottom; + _safeAreaHorizontal = + _mediaQueryData.padding.left + _mediaQueryData.padding.right; + _safeAreaVertical = + _mediaQueryData.padding.top + _mediaQueryData.padding.bottom; safeBlockHorizontal = (screenWidth - _safeAreaHorizontal) / 100; safeBlockVertical = (screenHeight - _safeAreaVertical) / 100; } else { blockSizeHorizontal = screenWidth / 120; blockSizeVertical = screenHeight / 120; - _safeAreaHorizontal = _mediaQueryData.padding.left + _mediaQueryData.padding.right; - _safeAreaVertical = _mediaQueryData.padding.top + _mediaQueryData.padding.bottom; + _safeAreaHorizontal = + _mediaQueryData.padding.left + _mediaQueryData.padding.right; + _safeAreaVertical = + _mediaQueryData.padding.top + _mediaQueryData.padding.bottom; safeBlockHorizontal = (screenWidth - _safeAreaHorizontal) / 120; safeBlockVertical = (screenHeight - _safeAreaVertical) / 120; } diff --git a/packages/dart/npt_flutter/lib/util/relay.dart b/packages/dart/npt_flutter/lib/util/relay.dart new file mode 100644 index 000000000..4df31f2f9 --- /dev/null +++ b/packages/dart/npt_flutter/lib/util/relay.dart @@ -0,0 +1,17 @@ +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter/material.dart'; + +class RelayUtil { + static Map getRelayDisplayNameMap(BuildContext context) { + final strings = AppLocalizations.of(context)!; + return { + "@rv_am": strings.rvAmDisplayName, + "@rv_ap": strings.rvApDisplayName, + "@rv_eu": strings.rvEuDisplayName, + }; + } + + static List getRelayAtsignList() { + return ["@rv_am", "@rv_ap", "@rv_eu"]; + } +} diff --git a/packages/dart/npt_flutter/lib/widgets/custom_card.dart b/packages/dart/npt_flutter/lib/widgets/custom_card.dart index 7b741b596..8c5dd2a01 100644 --- a/packages/dart/npt_flutter/lib/widgets/custom_card.dart +++ b/packages/dart/npt_flutter/lib/widgets/custom_card.dart @@ -23,10 +23,10 @@ class CustomCard extends StatelessWidget { const CustomCard.settingsRail({ required this.child, + this.height = Sizes.p436, + this.width = Sizes.p202, super.key, - }) : height = Sizes.p436, - width = Sizes.p202, - color = Colors.white, + }) : color = Colors.white, radiusTopLeft = const Radius.circular(Sizes.p10), radiusTopRight = const Radius.circular(Sizes.p10), radiusBottomLeft = const Radius.circular(Sizes.p10), @@ -39,10 +39,10 @@ class CustomCard extends StatelessWidget { const CustomCard.settingsContent({ required this.child, + this.height = Sizes.p470, + this.width = Sizes.p664, super.key, - }) : height = Sizes.p436, - width = Sizes.p664, - color = AppColor.cardColorDark, + }) : color = AppColor.cardColorDark, radiusTopLeft = Radius.zero, radiusTopRight = const Radius.circular(Sizes.p20), radiusBottomLeft = Radius.zero, @@ -54,9 +54,9 @@ class CustomCard extends StatelessWidget { bottomBorderSide = BorderSide.none; const CustomCard.profileFormContent({ required this.child, + this.height = Sizes.p500, super.key, - }) : height = Sizes.p450, - width = null, + }) : width = null, color = AppColor.cardColorDark, radiusTopLeft = const Radius.circular(Sizes.p20), radiusTopRight = const Radius.circular(Sizes.p20), @@ -69,10 +69,10 @@ class CustomCard extends StatelessWidget { bottomBorderSide = BorderSide.none; const CustomCard.dashboardContent({ required this.child, + this.height = Sizes.p500, + this.width = Sizes.p941, super.key, - }) : height = Sizes.p500, - width = Sizes.p941, - color = AppColor.cardColorDark, + }) : color = AppColor.cardColorDark, radiusTopLeft = const Radius.circular(Sizes.p20), radiusTopRight = const Radius.circular(Sizes.p20), radiusBottomLeft = const Radius.circular(Sizes.p20), diff --git a/packages/dart/npt_flutter/lib/widgets/custom_text_button.dart b/packages/dart/npt_flutter/lib/widgets/custom_text_button.dart index 454f4bfb7..f33930d3f 100644 --- a/packages/dart/npt_flutter/lib/widgets/custom_text_button.dart +++ b/packages/dart/npt_flutter/lib/widgets/custom_text_button.dart @@ -2,12 +2,17 @@ import 'package:at_contacts_flutter/services/contact_service.dart'; import 'package:at_onboarding_flutter/at_onboarding_flutter.dart'; import 'package:at_onboarding_flutter/services/onboarding_service.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:npt_flutter/constants.dart'; +import 'package:npt_flutter/features/onboarding/cubit/onboarding_cubit.dart'; +import 'package:npt_flutter/features/onboarding/util/atsign_manager.dart'; +import 'package:npt_flutter/features/onboarding/util/pre_offboard.dart'; +import 'package:npt_flutter/features/onboarding/widgets/onboarding_button.dart'; +import 'package:npt_flutter/pages/loading_page.dart'; import 'package:npt_flutter/routes.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../features/onboarding/onboarding.dart'; import '../styles/sizes.dart'; import 'custom_snack_bar.dart'; @@ -60,6 +65,11 @@ class CustomTextButton extends StatelessWidget { this.title = 'Reset App', this.type = CustomListTileType.resetAtsign, super.key}); + const CustomTextButton.signOut( + {this.iconData = Icons.logout_outlined, + this.title = 'Sign Out', + this.type = CustomListTileType.signOut, + super.key}); const CustomTextButton.feedback( {this.iconData = Icons.feedback_outlined, @@ -77,7 +87,7 @@ class CustomTextButton extends StatelessWidget { // final bodyMedium = Theme.of(context).textTheme.bodyMedium!; // final bodySmall = Theme.of(context).textTheme.bodySmall!; final strings = AppLocalizations.of(context)!; - Future onTap() async { + Future onTap({String? rootDomain}) async { switch (type) { case CustomListTileType.email: Uri emailUri = Uri( @@ -117,14 +127,14 @@ class CustomTextButton extends StatelessWidget { } break; case CustomListTileType.resetAtsign: - final futurePreference = await loadAtClientPreference(); + final futurePreference = await loadAtClientPreference(rootDomain!); if (context.mounted) { final result = await AtOnboarding.reset( context: context, config: AtOnboardingConfig( atClientPreference: futurePreference, rootEnvironment: RootEnvironment.Testing, - domain: Constants.rootDomain, + domain: rootDomain, appAPIKey: Constants.appAPIKey, ), ); @@ -147,6 +157,14 @@ class CustomTextButton extends StatelessWidget { if (!await launchUrl(emailUri)) { CustomSnackBar.notification(content: 'No email client available'); } + break; + + case CustomListTileType.signOut: + Navigator.of(context) + .pushAndRemoveUntil(MaterialPageRoute(builder: (context) => const LoadingPage()), (route) => false); + await preSignout(); + if (context.mounted) Navigator.of(context).pushReplacementNamed(Routes.onboarding); + break; } } @@ -168,14 +186,35 @@ class CustomTextButton extends StatelessWidget { return strings.resetAtsign; case CustomListTileType.feedback: return strings.feedback; + case CustomListTileType.signOut: + // TODO Localize in the next PR. + return 'Sign out'; } } + if (type == CustomListTileType.resetAtsign) { + return BlocBuilder(builder: (context, atsignInformation) { + return Padding( + padding: const EdgeInsets.only(left: Sizes.p30, right: Sizes.p30, bottom: Sizes.p10), + child: TextButton.icon( + label: Text(getTitle(strings)), + onPressed: () { + onTap(rootDomain: atsignInformation.rootDomain); + }, + icon: Icon( + iconData, + ), + ), + ); + }); + } return Padding( padding: const EdgeInsets.only(left: Sizes.p30, right: Sizes.p30, bottom: Sizes.p10), child: TextButton.icon( label: Text(getTitle(strings)), - onPressed: onTap, + onPressed: () { + onTap(); + }, icon: Icon( iconData, ), @@ -193,4 +232,5 @@ enum CustomListTileType { backupYourKey, resetAtsign, feedback, + signOut, } diff --git a/packages/dart/npt_flutter/lib/widgets/npt_app_bar.dart b/packages/dart/npt_flutter/lib/widgets/npt_app_bar.dart index 158fe05ef..8f9e9382e 100644 --- a/packages/dart/npt_flutter/lib/widgets/npt_app_bar.dart +++ b/packages/dart/npt_flutter/lib/widgets/npt_app_bar.dart @@ -10,8 +10,15 @@ class NptAppBar extends StatelessWidget implements PreferredSizeWidget { final String title; final Color? settingsSelectedColor; final bool isNavigateBack; + final bool showSettings; - const NptAppBar({super.key, required this.title, this.settingsSelectedColor, this.isNavigateBack = true}); + const NptAppBar({ + super.key, + this.title = '', + this.settingsSelectedColor, + this.isNavigateBack = true, + this.showSettings = true, + }); @override Size get preferredSize => Size.fromHeight(isNavigateBack ? Sizes.p150 : Sizes.p100); @@ -27,25 +34,27 @@ class NptAppBar extends StatelessWidget implements PreferredSizeWidget { children: [ Column( children: [ - gapH40, + gapH16, SvgPicture.asset( 'assets/noports_logo.svg', height: Sizes.p54, width: Sizes.p175, ), - gapH25, - TextButton.icon( - onPressed: () { - Navigator.pop(context); - }, - label: Text( - strings.back, - ), - icon: const Icon( - Icons.arrow_back_ios, - ), - style: StyleConstants.backButtonStyle, - ), + gapH16, + isNavigateBack + ? TextButton.icon( + onPressed: () { + Navigator.pop(context); + }, + label: Text( + strings.back, + ), + icon: const Icon( + Icons.arrow_back_ios, + ), + style: StyleConstants.backButtonStyle, + ) + : gap0, ], ), gapW27, @@ -71,13 +80,16 @@ class NptAppBar extends StatelessWidget implements PreferredSizeWidget { ], ), actions: [ - IconButton( - color: settingsSelectedColor, - icon: const Icon(Icons.settings_outlined), - onPressed: () { - Navigator.pushNamed(context, '/settings'); - }, - ), + showSettings + ? IconButton( + padding: const EdgeInsets.only(bottom: Sizes.p30), + color: settingsSelectedColor, + icon: const Icon(Icons.settings_outlined), + onPressed: () { + Navigator.pushNamed(context, '/settings'); + }, + ) + : gap0, ], centerTitle: true, ); diff --git a/packages/dart/npt_flutter/pubspec.lock b/packages/dart/npt_flutter/pubspec.lock index 3cc2e0107..8a461d80d 100644 --- a/packages/dart/npt_flutter/pubspec.lock +++ b/packages/dart/npt_flutter/pubspec.lock @@ -67,10 +67,10 @@ packages: dependency: transitive description: name: at_auth - sha256: "28f72f0fc26ec7f5f58d28fd29f964c9b2b35ecdc8dd4805ed7174851da2cbcc" + sha256: f4fec32e2a1ca8827604b5e54a7611ddad092c6ba607c138675c1cba5215b038 url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" at_backupkey_flutter: dependency: transitive description: @@ -91,26 +91,26 @@ packages: dependency: transitive description: name: at_chops - sha256: "825171a3132b3756119bd16b6fd1fa6257f74a64babaf13cae2d82d53b8c6be1" + sha256: "0b3d84b8bd2e5027946253d907ff23f967922105efe27432b15743beb74b31f8" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" at_client: dependency: transitive description: name: at_client - sha256: "2e98fe0c0c520b8e7ad6dfd0ad53ecb97f1ceb33c9b117dda69417b72a067c60" + sha256: "2c6aca2b3a2dab16b58330f99bdd00fe05bd05a76ffc5ed6b0d0eb34aaaaab8a" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" at_client_mobile: dependency: "direct main" description: name: at_client_mobile - sha256: "749b686cf403d4f396fbce5b7684574d983669274553950d92542b50830e9d28" + sha256: "41f45cc094bfc7748303ce263e490d9744bb6014e4c1183df9e983488714fb7e" url: "https://pub.dev" source: hosted - version: "3.2.18" + version: "3.2.19" at_common_flutter: dependency: transitive description: @@ -123,26 +123,26 @@ packages: dependency: transitive description: name: at_commons - sha256: "2d0490a0c5bcd43c6a37911d85b71c133767aec47abc65bd8ecb20c8caaddeab" + sha256: "796eb7f49ab8894782010146368b4ae4f9ed716f2174c29c37d5c53b81281ff6" url: "https://pub.dev" source: hosted - version: "4.0.11" + version: "5.0.0" at_contact: dependency: "direct main" description: name: at_contact - sha256: e1b8904116e6e0fcbc5627409bffe3b620417c62b76bbedc84b1f66acc28adfe + sha256: e67a3545f2df3f0c8e28f0360c8cb301c1677043cd4b797c8d78dc92a69f2e62 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" at_contacts_flutter: dependency: "direct main" description: name: at_contacts_flutter - sha256: "530a5112e303fdf8ae26bfbe477112b14c16c8b35f7005ea4919f05584fa8dec" + sha256: e39b77b302c5e12ec7a543b8f1f6b65eed42c36d153f8298dd3b52d7d1ad2051 url: "https://pub.dev" source: hosted - version: "4.0.15" + version: "4.0.16" at_demo_data: dependency: transitive description: @@ -163,27 +163,27 @@ packages: dependency: transitive description: name: at_lookup - sha256: e989099d5f2cd6415097c8e4353340bd2048c9ee1bc82665f2b4f7c4615ad055 + sha256: "2fa727fbdd6d3e5a79132786a74cbf03776833e1671f8cb471d21585f8448f95" url: "https://pub.dev" source: hosted - version: "3.0.47" + version: "3.0.49" at_onboarding_flutter: dependency: "direct main" description: path: "packages/at_onboarding_flutter" ref: trunk - resolved-ref: "8df0a468041dd332534acf5e5e487e018f6ba2da" + resolved-ref: "2a32ac2461673e0df16f5de2e24305309a8fcd95" url: "https://github.com/atsign-foundation/at_widgets" source: git - version: "6.1.8" + version: "6.1.9" at_persistence_secondary_server: dependency: transitive description: name: at_persistence_secondary_server - sha256: "1ec73b56e61b8aee94104ad4610c17cf07e366239337bedd43fa80c7765a391d" + sha256: "387ff2853ee98a8c65526e1df9220fa58c4631b9b1cd6002e9a7372f1a491ed3" url: "https://pub.dev" source: hosted - version: "3.0.63" + version: "3.0.64" at_persistence_spec: dependency: transitive description: @@ -196,10 +196,10 @@ packages: dependency: transitive description: name: at_server_status - sha256: "316c3e6717592677207d4f0a836b013271ca0f729e8b575c9195d19cfc57e71b" + sha256: "2773fa7c4377802b671f6854863214aabe8ee8cd49be87226352dd14562a5d6b" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" at_sync_ui_flutter: dependency: transitive description: @@ -220,10 +220,10 @@ packages: dependency: "direct main" description: name: at_utils - sha256: ec28600e4eec321ee5e22be051109fa7b2e94590dc51d9f957730c2540beb681 + sha256: b4461b0743f323429d57c387e91186537df8a6aeb4608bbeb6c2adf01d9f08f9 url: "https://pub.dev" source: hosted - version: "3.0.16" + version: "3.0.19" biometric_storage: dependency: transitive description: @@ -899,7 +899,7 @@ packages: path: "../noports_core" relative: true source: path - version: "6.1.0" + version: "6.2.0" openssh_ed25519: dependency: transitive description: @@ -933,7 +933,7 @@ packages: source: hosted version: "3.0.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" @@ -1509,10 +1509,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: transitive description: diff --git a/packages/dart/npt_flutter/pubspec.yaml b/packages/dart/npt_flutter/pubspec.yaml index a2e490743..e698155a1 100644 --- a/packages/dart/npt_flutter/pubspec.yaml +++ b/packages/dart/npt_flutter/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: meta: ^1.15.0 noports_core: path: ../noports_core + path: ^1.9.0 path_provider: ^2.1.4 phosphor_flutter: ^2.1.0 socket_connector: ^2.2.0