Skip to content

Commit

Permalink
Merge pull request #1431 from atsign-foundation/npt-flutter-onboardin…
Browse files Browse the repository at this point in the history
…g-utils

feat: npt flutter onboarding utils
  • Loading branch information
CurtlyCritchlow authored Oct 7, 2024
2 parents 48c6d17 + a3712ca commit 35e008d
Show file tree
Hide file tree
Showing 18 changed files with 325 additions and 98 deletions.
15 changes: 9 additions & 6 deletions packages/dart/npt_flutter/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class App extends StatelessWidget {
],
child: MultiBlocProvider(
providers: [
// TODO this should be called LocalSettingsCubit and move
// Localization from the SettingsCubit to this
BlocProvider<EnableLoggingCubit>(
create: (_) => EnableLoggingCubit(),
),
Expand All @@ -48,6 +50,11 @@ class App extends StatelessWidget {
create: (_) => OnboardingCubit(),
),

// A bloc which manages the atDirectory state
BlocProvider<AtDirectoryCubit>(
create: (_) => AtDirectoryCubit(),
),

/// Settings provider, not much else to say
/// - If settings are not found, we automatically load some defaults
/// so it is possible that someone's settings get wiped if there is
Expand Down Expand Up @@ -89,13 +96,9 @@ class App extends StatelessWidget {
BlocProvider<FavoriteBloc>(
create: (ctx) => FavoriteBloc(ctx.read<FavoriteRepository>()),
),

// A bloc which manages the atDirectory state
BlocProvider<AtDirectoryCubit>(
create: (_) => AtDirectoryCubit(),
),
],
child: BlocSelector<SettingsBloc, SettingsState, Language>(selector: (state) {
child: BlocSelector<SettingsBloc, SettingsState, Language>(
selector: (state) {
if (state is SettingsLoadedState) {
return state.settings.language;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class FavoriteBloc extends LoggingBloc<FavoriteEvent, FavoritesState> {
on<FavoriteRemoveEvent>(_onRemove);
}

void clearAll() => emit(const FavoritesInitial());

FutureOr<void> _onLoad(
FavoriteLoadEvent event, Emitter<FavoritesState> emit) async {
emit(const FavoritesLoading());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:npt_flutter/features/logging/models/loggable.dart';
import 'package:npt_flutter/features/logging/models/logging_bloc.dart';

class AtDirectoryCubit extends Cubit<String> {
AtDirectoryCubit() : super('root.atsign.org');
class AtDirectoryCubit extends LoggingCubit<LoggableString> {
AtDirectoryCubit() : super(const LoggableString('root.atsign.org'));

void setRootDomain(String rootDomain) => emit(rootDomain);
void setRootDomain(String rootDomain) => emit(LoggableString(rootDomain));
String getRootDomain() => (state.string);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
abstract class AtsignInformation {
String get atSign;
String get rootDomain;
import 'dart:convert';
import 'dart:io';

import 'package:at_onboarding_flutter/at_onboarding_flutter.dart';
import 'package:npt_flutter/app.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

class AtsignInformation {
final String atSign;
final String rootDomain;

AtsignInformation({required this.atSign, required this.rootDomain});

Map<String, String> 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"],
);
}
}

// This will return a map which looks like:
Expand All @@ -19,12 +44,119 @@ abstract class AtsignInformation {
// Now you have the rootDomain for the existing atSign and can use it to onboard
// correctly

Future<Map<String, AtsignInformation>> getAtsignEntries() {
return Future.value({});
Future<Map<String, AtsignInformation>> getAtsignEntries() async {
var keychainAtSigns = await KeychainUtil.getAtsignList() ?? [];
var atSignInfo = <AtsignInformation>[];
try {
atSignInfo = await _getAtsignInformationFromFile();
} catch (e) {
App.log(
"Failed get Atsign Information, ignoring invalid file: ${e.toString()}"
.loggable,
);
return {};
}
var atSignMap = <String, AtsignInformation>{};
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<bool> saveAtsignInformation(AtsignInformation info) {
return Future.value(true);
Future<bool> saveAtsignInformation(AtsignInformation info) async {
var f = await _getAtsignInformationFile();
final List<AtsignInformation> 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())),
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<File?> _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<List<AtsignInformation>> _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();
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 = <AtsignInformation>[];
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;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +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<bool> preSignout(String atSign) async {
App.log("Resetting all application state before signout".loggable);
// We need to do the following before "signing out"
// - Wipe all application state
// - Remove the tray icon
App.navState.currentContext?.read<ProfilesRunningCubit>().stopAllAndClear();
App.navState.currentContext?.read<ProfileCacheCubit>().clear();
App.navState.currentContext?.read<ProfilesSelectedCubit>().deselectAll();
App.navState.currentContext?.read<FavoriteBloc>().clearAll();
App.navState.currentContext?.read<ProfileListBloc>().clearAll();
App.navState.currentContext?.read<SettingsBloc>().clear();
App.navState.currentContext?.read<OnboardingCubit>().offboard();
// - Reset the tray icon
App.navState.currentContext?.read<TrayCubit>().initialize();
return true;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:npt_flutter/features/logging/models/loggable.dart';
import 'package:npt_flutter/features/onboarding/cubit/at_directory_cubit.dart';

typedef OnboardingMapCallback = void Function(Map<String, String> val);
Expand All @@ -17,16 +18,19 @@ class OnboardingAtDirectorySelector extends StatelessWidget {

@override
Widget build(BuildContext context) {
return BlocBuilder<AtDirectoryCubit, String>(builder: (context, rootDomain) {
controller.text = rootDomain;
return BlocBuilder<AtDirectoryCubit, LoggableString>(
builder: (context, rootDomain) {
controller.text = rootDomain.string;
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: DropdownMenu<String>(
initialSelection: options.contains(rootDomain) ? rootDomain : null,
initialSelection: options.contains(rootDomain.string)
? rootDomain.string
: null,
dropdownMenuEntries: options
.map<DropdownMenuEntry<String>>(
(o) => DropdownMenuEntry(
Expand Down Expand Up @@ -56,7 +60,9 @@ class OnboardingAtDirectorySelector extends StatelessWidget {
// validator: FormValidator.validateRequiredAtsignField,
onChanged: (value) {
// prevent the user from adding the default values to the dropdown a second time.
if (value != options[0] || value != options[1]) options.add(value);
if (value != options[0] || value != options[1]) {
options.add(value);
}
//removes the third element making the final entry the only additional value in options. This prevents the dropdown from having more than 3 entries.
if (options.length > 3) options.removeAt(2);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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/logging/models/loggable.dart';
import 'package:npt_flutter/features/onboarding/cubit/at_directory_cubit.dart';
import 'package:npt_flutter/features/onboarding/onboarding.dart';
import 'package:npt_flutter/features/onboarding/widgets/at_directory_dialog.dart';
Expand Down Expand Up @@ -35,12 +36,13 @@ class _OnboardingButtonState extends State<OnboardingButton> {
@override
Widget build(BuildContext context) {
final strings = AppLocalizations.of(context)!;
return BlocBuilder<AtDirectoryCubit, String>(builder: (context, rootDomain) {
return BlocBuilder<AtDirectoryCubit, LoggableString>(
builder: (context, rootDomain) {
return ElevatedButton.icon(
onPressed: () async {
final result = await selectOptions();

if (result && context.mounted) onboard(rootDomain: context.read<AtDirectoryCubit>().state);
if (result && context.mounted) onboard(rootDomain: rootDomain.string);
},
icon: PhosphorIcon(PhosphorIcons.arrowUpRight()),
label: Text(
Expand All @@ -51,7 +53,8 @@ class _OnboardingButtonState extends State<OnboardingButton> {
});
}

Future<void> onboard({required String rootDomain, bool isFromInitState = false}) async {
Future<void> onboard(
{required String rootDomain, bool isFromInitState = false}) async {
AtOnboardingResult onboardingResult = await AtOnboarding.onboard(
// ignore: use_build_context_synchronously
context: context,
Expand All @@ -68,7 +71,9 @@ class _OnboardingButtonState extends State<OnboardingButton> {
case AtOnboardingResultStatus.success:
await initializeContactsService(rootDomain: rootDomain);
postOnboard(onboardingResult.atsign!);
Navigator.of(context).pushReplacementNamed(Routes.dashboard);
if (mounted) {
Navigator.of(context).pushReplacementNamed(Routes.dashboard);
}
break;
case AtOnboardingResultStatus.error:
if (isFromInitState) break;
Expand Down
Loading

0 comments on commit 35e008d

Please sign in to comment.