refactor some more

This commit is contained in:
Christien Rioux 2023-12-27 22:56:24 -05:00
parent 2adc958128
commit c516323e7d
91 changed files with 1237 additions and 748 deletions

View file

@ -0,0 +1,3 @@
export 'cubit/cubit.dart';
export 'repository/repository.dart';
export 'view/view.dart';

View file

@ -0,0 +1,42 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../repository/account_repository/account_repository.dart';
part 'active_user_login_state.dart';
class ActiveUserLoginCubit extends Cubit<ActiveUserLoginState> {
ActiveUserLoginCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(null) {
// Subscribe to streams
_initAccountRepositorySubscription();
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
switch (change) {
case AccountRepositoryChange.activeUserLogin:
emit(_accountRepository.getActiveUserLogin());
break;
// Ignore these
case AccountRepositoryChange.localAccounts:
case AccountRepositoryChange.userLogins:
break;
}
});
}
@override
Future<void> close() async {
await super.close();
await _accountRepositorySubscription.cancel();
}
final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange>
_accountRepositorySubscription;
}

View file

@ -0,0 +1,3 @@
part of 'active_user_login_cubit.dart';
typedef ActiveUserLoginState = TypedKey?;

View file

@ -0,0 +1,3 @@
export 'active_user_login_cubit/active_user_login_cubit.dart';
export 'local_accounts_cubit/local_accounts_cubit.dart';
export 'user_logins_cubit/user_logins_cubit.dart';

View file

@ -0,0 +1,42 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../repository/account_repository/account_repository.dart';
part 'local_accounts_state.dart';
class LocalAccountsCubit extends Cubit<LocalAccountsState> {
LocalAccountsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(LocalAccountsState()) {
// Subscribe to streams
_initAccountRepositorySubscription();
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
switch (change) {
case AccountRepositoryChange.localAccounts:
emit(_accountRepository.getLocalAccounts());
break;
// Ignore these
case AccountRepositoryChange.userLogins:
case AccountRepositoryChange.activeUserLogin:
break;
}
});
}
@override
Future<void> close() async {
await super.close();
await _accountRepositorySubscription.cancel();
}
final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange>
_accountRepositorySubscription;
}

View file

@ -0,0 +1,3 @@
part of 'local_accounts_cubit.dart';
typedef LocalAccountsState = IList<LocalAccount>;

View file

@ -0,0 +1,42 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../../repository/account_repository/account_repository.dart';
part 'user_logins_state.dart';
class UserLoginsCubit extends Cubit<UserLoginsState> {
UserLoginsCubit(AccountRepository accountRepository)
: _accountRepository = accountRepository,
super(UserLoginsState()) {
// Subscribe to streams
_initAccountRepositorySubscription();
}
void _initAccountRepositorySubscription() {
_accountRepositorySubscription =
_accountRepository.changes().listen((change) {
switch (change) {
case AccountRepositoryChange.userLogins:
emit(_accountRepository.getUserLogins());
break;
// Ignore these
case AccountRepositoryChange.localAccounts:
case AccountRepositoryChange.activeUserLogin:
break;
}
});
}
@override
Future<void> close() async {
await super.close();
await _accountRepositorySubscription.cancel();
}
final AccountRepository _accountRepository;
late final StreamSubscription<AccountRepositoryChange>
_accountRepositorySubscription;
}

View file

@ -0,0 +1,3 @@
part of 'user_logins_cubit.dart';
typedef UserLoginsState = IList<UserLogin>;

View file

@ -0,0 +1,297 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
import 'active_logins.dart';
import 'encryption_key_type.dart';
import 'local_account.dart';
import 'new_profile_spec.dart';
import 'user_login.dart';
export 'active_logins.dart';
export 'encryption_key_type.dart';
export 'local_account.dart';
export 'user_login.dart';
const String veilidChatAccountKey = 'com.veilid.veilidchat';
enum AccountRepositoryChange { localAccounts, userLogins, activeUserLogin }
class AccountRepository {
AccountRepository._()
: _localAccounts = _initLocalAccounts(),
_activeLogins = _initActiveLogins();
static TableDBValue<IList<LocalAccount>> _initLocalAccounts() => TableDBValue(
tableName: 'local_account_manager',
tableKeyName: 'local_accounts',
valueFromJson: (obj) => obj != null
? IList<LocalAccount>.fromJson(
obj, genericFromJson(LocalAccount.fromJson))
: IList<LocalAccount>(),
valueToJson: (val) => val.toJson((la) => la.toJson()));
static TableDBValue<ActiveLogins> _initActiveLogins() => TableDBValue(
tableName: 'local_account_manager',
tableKeyName: 'active_logins',
valueFromJson: (obj) => obj != null
? ActiveLogins.fromJson(obj as Map<String, dynamic>)
: ActiveLogins.empty(),
valueToJson: (val) => val.toJson());
final TableDBValue<IList<LocalAccount>> _localAccounts;
final TableDBValue<ActiveLogins> _activeLogins;
//////////////////////////////////////////////////////////////
/// Singleton initialization
static AccountRepository instance = AccountRepository._();
Future<void> init() async {
await _localAccounts.load();
await _activeLogins.load();
}
//////////////////////////////////////////////////////////////
/// Streams
Stream<AccountRepositoryChange> changes() async* {}
//////////////////////////////////////////////////////////////
/// Selectors
IList<LocalAccount> getLocalAccounts() => _localAccounts.requireValue;
IList<UserLogin> getUserLogins() => _activeLogins.requireValue.userLogins;
TypedKey? getActiveUserLogin() => _activeLogins.requireValue.activeUserLogin;
LocalAccount? fetchLocalAccount({required TypedKey accountMasterRecordKey}) {
final localAccounts = _localAccounts.requireValue;
final idx = localAccounts.indexWhere(
(e) => e.identityMaster.masterRecordKey == accountMasterRecordKey);
if (idx == -1) {
return null;
}
return localAccounts[idx];
}
UserLogin? fetchLogin({required TypedKey accountMasterRecordKey}) {
final userLogins = _activeLogins.requireValue.userLogins;
final idx = userLogins
.indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey);
if (idx == -1) {
return null;
}
return userLogins[idx];
}
//////////////////////////////////////////////////////////////
/// Mutators
/// Reorder accounts
Future<void> reorderAccount(int oldIndex, int newIndex) async {
final localAccounts = await _localAccounts.get();
final removedItem = Output<LocalAccount>();
final updated = localAccounts
.removeAt(oldIndex, removedItem)
.insert(newIndex, removedItem.value!);
await _localAccounts.set(updated);
}
/// Creates a new master identity, an account associated with the master
/// identity, stores the account in the identity key and then logs into
/// that account with no password set at this time
Future<void> createMasterIdentity(NewProfileSpec newProfileSpec) async {
final imws = await IdentityMasterWithSecrets.create();
try {
final localAccount = await _newLocalAccount(
identityMaster: imws.identityMaster,
identitySecret: imws.identitySecret,
newProfileSpec: newProfileSpec);
// Log in the new account by default with no pin
final ok = await login(localAccount.identityMaster.masterRecordKey,
EncryptionKeyType.none, '');
assert(ok, 'login with none should never fail');
} on Exception catch (_) {
await imws.delete();
rethrow;
}
}
/// Creates a new Account associated with master identity
/// Adds a logged-out LocalAccount to track its existence on this device
Future<LocalAccount> _newLocalAccount(
{required IdentityMaster identityMaster,
required SecretKey identitySecret,
required NewProfileSpec newProfileSpec,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
String encryptionKey = ''}) async {
final localAccounts = await _localAccounts.get();
// Add account with profile to DHT
await identityMaster.addAccountToIdentity(
identitySecret: identitySecret,
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
final contactList = await (await DHTShortArray.create(parent: parent))
.scope((r) async => r.record.ownedDHTRecordPointer);
// Make empty contact invitation record list
final contactInvitationRecords =
await (await DHTShortArray.create(parent: parent))
.scope((r) async => r.record.ownedDHTRecordPointer);
// Make empty chat record list
final chatRecords = await (await DHTShortArray.create(parent: parent))
.scope((r) async => r.record.ownedDHTRecordPointer);
// Make account object
final account = proto.Account()
..profile = (proto.Profile()
..name = newProfileSpec.name
..pronouns = newProfileSpec.pronouns)
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
return account;
});
// Encrypt identitySecret with key
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
cryptoKind: identityMaster.identityRecordKey.kind,
encryptionKey: encryptionKey,
);
// Create local account object
// Does not contain the account key or its secret
// as that is not to be persisted, and only pulled from the identity key
// and optionally decrypted with the unlock password
final localAccount = LocalAccount(
identityMaster: identityMaster,
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
hiddenAccount: false,
name: newProfileSpec.name,
);
// Add local account object to internal store
final newLocalAccounts = localAccounts.add(localAccount);
await _localAccounts.set(newLocalAccounts);
// Return local account object
return localAccount;
}
/// Remove an account and wipe the messages for this account from this device
Future<bool> deleteLocalAccount(TypedKey accountMasterRecordKey) async {
await logout(accountMasterRecordKey);
final localAccounts = await _localAccounts.get();
final newLocalAccounts = localAccounts.removeWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
await _localAccounts.set(newLocalAccounts);
// TO DO: wipe messages
return true;
}
/// Import an account from another VeilidChat instance
/// Recover an account with the master identity secret
/// Delete an account from all devices
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
final activeLogins = await _activeLogins.get();
if (accountMasterRecordKey != null) {
// Assert the specified record key can be found, will throw if not
final _ = activeLogins.userLogins.firstWhere(
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
}
final newActiveLogins =
activeLogins.copyWith(activeUserLogin: accountMasterRecordKey);
await _activeLogins.set(newActiveLogins);
}
Future<bool> _decryptedLogin(
IdentityMaster identityMaster, SecretKey identitySecret) async {
final cs = await Veilid.instance
.getCryptoSystem(identityMaster.identityRecordKey.kind);
final keyOk = await cs.validateKeyPair(
identityMaster.identityPublicKey, identitySecret);
if (!keyOk) {
throw Exception('Identity is corrupted');
}
// Read the identity key to get the account keys
final accountRecordInfo = await identityMaster.readAccountFromIdentity(
identitySecret: identitySecret, accountKey: veilidChatAccountKey);
// Add to user logins and select it
final activeLogins = await _activeLogins.get();
final now = Veilid.instance.now();
final newActiveLogins = activeLogins.copyWith(
userLogins: activeLogins.userLogins.replaceFirstWhere(
(ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey,
(ul) => ul != null
? ul.copyWith(lastActive: now)
: UserLogin(
accountMasterRecordKey: identityMaster.masterRecordKey,
identitySecret:
TypedSecret(kind: cs.kind(), value: identitySecret),
accountRecordInfo: accountRecordInfo,
lastActive: now),
addIfNotFound: true),
activeUserLogin: identityMaster.masterRecordKey);
await _activeLogins.set(newActiveLogins);
return true;
}
Future<bool> login(TypedKey accountMasterRecordKey,
EncryptionKeyType encryptionKeyType, String encryptionKey) async {
final localAccounts = await _localAccounts.get();
// Get account, throws if not found
final localAccount = localAccounts.firstWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
// Log in with this local account
// Derive key from password
if (localAccount.encryptionKeyType != encryptionKeyType) {
throw Exception('Wrong authentication type');
}
final identitySecret =
await localAccount.encryptionKeyType.decryptSecretFromBytes(
secretBytes: localAccount.identitySecretBytes,
cryptoKind: localAccount.identityMaster.identityRecordKey.kind,
encryptionKey: encryptionKey,
);
// Validate this secret with the identity public key and log in
return _decryptedLogin(localAccount.identityMaster, identitySecret);
}
Future<void> logout(TypedKey? accountMasterRecordKey) async {
final activeLogins = await _activeLogins.get();
final logoutUser = accountMasterRecordKey ?? activeLogins.activeUserLogin;
if (logoutUser == null) {
return;
}
final newActiveLogins = activeLogins.copyWith(
activeUserLogin: activeLogins.activeUserLogin == logoutUser
? null
: activeLogins.activeUserLogin,
userLogins: activeLogins.userLogins
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser));
await _activeLogins.set(newActiveLogins);
}
}

View file

@ -0,0 +1,25 @@
// Represents a set of user logins and the currently selected account
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:veilid_support/veilid_support.dart';
import 'user_login.dart';
part 'active_logins.g.dart';
part 'active_logins.freezed.dart';
@freezed
class ActiveLogins with _$ActiveLogins {
const factory ActiveLogins({
// The list of current logged in accounts
required IList<UserLogin> userLogins,
// The current selected account indexed by master record key
TypedKey? activeUserLogin,
}) = _ActiveLogins;
factory ActiveLogins.empty() =>
const ActiveLogins(userLogins: IListConst([]));
factory ActiveLogins.fromJson(dynamic json) =>
_ActiveLogins.fromJson(json as Map<String, dynamic>);
}

View file

@ -0,0 +1,181 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'active_logins.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
ActiveLogins _$ActiveLoginsFromJson(Map<String, dynamic> json) {
return _ActiveLogins.fromJson(json);
}
/// @nodoc
mixin _$ActiveLogins {
// The list of current logged in accounts
IList<UserLogin> get userLogins =>
throw _privateConstructorUsedError; // The current selected account indexed by master record key
Typed<FixedEncodedString43>? get activeUserLogin =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ActiveLoginsCopyWith<ActiveLogins> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ActiveLoginsCopyWith<$Res> {
factory $ActiveLoginsCopyWith(
ActiveLogins value, $Res Function(ActiveLogins) then) =
_$ActiveLoginsCopyWithImpl<$Res, ActiveLogins>;
@useResult
$Res call(
{IList<UserLogin> userLogins,
Typed<FixedEncodedString43>? activeUserLogin});
}
/// @nodoc
class _$ActiveLoginsCopyWithImpl<$Res, $Val extends ActiveLogins>
implements $ActiveLoginsCopyWith<$Res> {
_$ActiveLoginsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? userLogins = null,
Object? activeUserLogin = freezed,
}) {
return _then(_value.copyWith(
userLogins: null == userLogins
? _value.userLogins
: userLogins // ignore: cast_nullable_to_non_nullable
as IList<UserLogin>,
activeUserLogin: freezed == activeUserLogin
? _value.activeUserLogin
: activeUserLogin // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>?,
) as $Val);
}
}
/// @nodoc
abstract class _$$ActiveLoginsImplCopyWith<$Res>
implements $ActiveLoginsCopyWith<$Res> {
factory _$$ActiveLoginsImplCopyWith(
_$ActiveLoginsImpl value, $Res Function(_$ActiveLoginsImpl) then) =
__$$ActiveLoginsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{IList<UserLogin> userLogins,
Typed<FixedEncodedString43>? activeUserLogin});
}
/// @nodoc
class __$$ActiveLoginsImplCopyWithImpl<$Res>
extends _$ActiveLoginsCopyWithImpl<$Res, _$ActiveLoginsImpl>
implements _$$ActiveLoginsImplCopyWith<$Res> {
__$$ActiveLoginsImplCopyWithImpl(
_$ActiveLoginsImpl _value, $Res Function(_$ActiveLoginsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? userLogins = null,
Object? activeUserLogin = freezed,
}) {
return _then(_$ActiveLoginsImpl(
userLogins: null == userLogins
? _value.userLogins
: userLogins // ignore: cast_nullable_to_non_nullable
as IList<UserLogin>,
activeUserLogin: freezed == activeUserLogin
? _value.activeUserLogin
: activeUserLogin // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ActiveLoginsImpl implements _ActiveLogins {
const _$ActiveLoginsImpl({required this.userLogins, this.activeUserLogin});
factory _$ActiveLoginsImpl.fromJson(Map<String, dynamic> json) =>
_$$ActiveLoginsImplFromJson(json);
// The list of current logged in accounts
@override
final IList<UserLogin> userLogins;
// The current selected account indexed by master record key
@override
final Typed<FixedEncodedString43>? activeUserLogin;
@override
String toString() {
return 'ActiveLogins(userLogins: $userLogins, activeUserLogin: $activeUserLogin)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ActiveLoginsImpl &&
const DeepCollectionEquality()
.equals(other.userLogins, userLogins) &&
(identical(other.activeUserLogin, activeUserLogin) ||
other.activeUserLogin == activeUserLogin));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType,
const DeepCollectionEquality().hash(userLogins), activeUserLogin);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith =>
__$$ActiveLoginsImplCopyWithImpl<_$ActiveLoginsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ActiveLoginsImplToJson(
this,
);
}
}
abstract class _ActiveLogins implements ActiveLogins {
const factory _ActiveLogins(
{required final IList<UserLogin> userLogins,
final Typed<FixedEncodedString43>? activeUserLogin}) = _$ActiveLoginsImpl;
factory _ActiveLogins.fromJson(Map<String, dynamic> json) =
_$ActiveLoginsImpl.fromJson;
@override // The list of current logged in accounts
IList<UserLogin> get userLogins;
@override // The current selected account indexed by master record key
Typed<FixedEncodedString43>? get activeUserLogin;
@override
@JsonKey(ignore: true)
_$$ActiveLoginsImplCopyWith<_$ActiveLoginsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'active_logins.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ActiveLoginsImpl _$$ActiveLoginsImplFromJson(Map<String, dynamic> json) =>
_$ActiveLoginsImpl(
userLogins: IList<UserLogin>.fromJson(
json['user_logins'], (value) => UserLogin.fromJson(value)),
activeUserLogin: json['active_user_login'] == null
? null
: Typed<FixedEncodedString43>.fromJson(json['active_user_login']),
);
Map<String, dynamic> _$$ActiveLoginsImplToJson(_$ActiveLoginsImpl instance) =>
<String, dynamic>{
'user_logins': instance.userLogins.toJson(
(value) => value.toJson(),
),
'active_user_login': instance.activeUserLogin?.toJson(),
};

View file

@ -0,0 +1,79 @@
// Local account identitySecretKey is potentially encrypted with a key
// using the following mechanisms
// * None : no key, bytes are unencrypted
// * Pin : Code is a numeric pin (4-256 numeric digits) hashed with Argon2
// * Password: Code is a UTF-8 string that is hashed with Argon2
import 'dart:typed_data';
import 'package:change_case/change_case.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../../proto/proto.dart' as proto;
enum EncryptionKeyType {
none,
pin,
password;
factory EncryptionKeyType.fromJson(dynamic j) =>
EncryptionKeyType.values.byName((j as String).toCamelCase());
factory EncryptionKeyType.fromProto(proto.EncryptionKeyType p) {
// ignore: exhaustive_cases
switch (p) {
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE:
return EncryptionKeyType.none;
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN:
return EncryptionKeyType.pin;
case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD:
return EncryptionKeyType.password;
}
throw StateError('unknown EncryptionKeyType enum value');
}
String toJson() => name.toPascalCase();
proto.EncryptionKeyType toProto() => switch (this) {
EncryptionKeyType.none =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE,
EncryptionKeyType.pin =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN,
EncryptionKeyType.password =>
proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD,
};
Future<Uint8List> encryptSecretToBytes(
{required SecretKey secret,
required CryptoKind cryptoKind,
String encryptionKey = ''}) async {
late final Uint8List secretBytes;
switch (this) {
case EncryptionKeyType.none:
secretBytes = secret.decode();
case EncryptionKeyType.pin:
case EncryptionKeyType.password:
final cs = await Veilid.instance.getCryptoSystem(cryptoKind);
secretBytes =
await cs.encryptAeadWithPassword(secret.decode(), encryptionKey);
}
return secretBytes;
}
Future<SecretKey> decryptSecretFromBytes(
{required Uint8List secretBytes,
required CryptoKind cryptoKind,
String encryptionKey = ''}) async {
late final SecretKey secret;
switch (this) {
case EncryptionKeyType.none:
secret = SecretKey.fromBytes(secretBytes);
case EncryptionKeyType.pin:
case EncryptionKeyType.password:
final cs = await Veilid.instance.getCryptoSystem(cryptoKind);
secret = SecretKey.fromBytes(
await cs.decryptAeadWithPassword(secretBytes, encryptionKey));
}
return secret;
}
}

View file

@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:veilid_support/veilid_support.dart';
import 'encryption_key_type.dart';
part 'local_account.g.dart';
part 'local_account.freezed.dart';
// Local Accounts are stored in a table locally and not backed by a DHT key
// and represents the accounts that have been added/imported
// on the current device.
// Stores a copy of the IdentityMaster associated with the account
// and the identitySecretKey optionally encrypted by an unlock code
// This is the root of the account information tree for VeilidChat
//
@freezed
class LocalAccount with _$LocalAccount {
const factory LocalAccount({
// The master key record for the account, containing the identityPublicKey
required IdentityMaster identityMaster,
// The encrypted identity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter() required Uint8List identitySecretBytes,
// The kind of encryption input used on the account
required EncryptionKeyType encryptionKeyType,
// If account is not hidden, password can be retrieved via
required bool biometricsEnabled,
// Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
required bool hiddenAccount,
// Display name for account until it is unlocked
required String name,
}) = _LocalAccount;
factory LocalAccount.fromJson(dynamic json) =>
_$LocalAccountFromJson(json as Map<String, dynamic>);
}

View file

@ -0,0 +1,301 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'local_account.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
LocalAccount _$LocalAccountFromJson(Map<String, dynamic> json) {
return _LocalAccount.fromJson(json);
}
/// @nodoc
mixin _$LocalAccount {
// The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster =>
throw _privateConstructorUsedError; // The encrypted identity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes =>
throw _privateConstructorUsedError; // The kind of encryption input used on the account
EncryptionKeyType get encryptionKeyType =>
throw _privateConstructorUsedError; // If account is not hidden, password can be retrieved via
bool get biometricsEnabled =>
throw _privateConstructorUsedError; // Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
bool get hiddenAccount =>
throw _privateConstructorUsedError; // Display name for account until it is unlocked
String get name => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$LocalAccountCopyWith<LocalAccount> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LocalAccountCopyWith<$Res> {
factory $LocalAccountCopyWith(
LocalAccount value, $Res Function(LocalAccount) then) =
_$LocalAccountCopyWithImpl<$Res, LocalAccount>;
@useResult
$Res call(
{IdentityMaster identityMaster,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
bool hiddenAccount,
String name});
$IdentityMasterCopyWith<$Res> get identityMaster;
}
/// @nodoc
class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
implements $LocalAccountCopyWith<$Res> {
_$LocalAccountCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
Object? hiddenAccount = null,
Object? name = null,
}) {
return _then(_value.copyWith(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
as Uint8List,
encryptionKeyType: null == encryptionKeyType
? _value.encryptionKeyType
: encryptionKeyType // ignore: cast_nullable_to_non_nullable
as EncryptionKeyType,
biometricsEnabled: null == biometricsEnabled
? _value.biometricsEnabled
: biometricsEnabled // ignore: cast_nullable_to_non_nullable
as bool,
hiddenAccount: null == hiddenAccount
? _value.hiddenAccount
: hiddenAccount // ignore: cast_nullable_to_non_nullable
as bool,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$IdentityMasterCopyWith<$Res> get identityMaster {
return $IdentityMasterCopyWith<$Res>(_value.identityMaster, (value) {
return _then(_value.copyWith(identityMaster: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$LocalAccountImplCopyWith<$Res>
implements $LocalAccountCopyWith<$Res> {
factory _$$LocalAccountImplCopyWith(
_$LocalAccountImpl value, $Res Function(_$LocalAccountImpl) then) =
__$$LocalAccountImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{IdentityMaster identityMaster,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
bool hiddenAccount,
String name});
@override
$IdentityMasterCopyWith<$Res> get identityMaster;
}
/// @nodoc
class __$$LocalAccountImplCopyWithImpl<$Res>
extends _$LocalAccountCopyWithImpl<$Res, _$LocalAccountImpl>
implements _$$LocalAccountImplCopyWith<$Res> {
__$$LocalAccountImplCopyWithImpl(
_$LocalAccountImpl _value, $Res Function(_$LocalAccountImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
Object? hiddenAccount = null,
Object? name = null,
}) {
return _then(_$LocalAccountImpl(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
as Uint8List,
encryptionKeyType: null == encryptionKeyType
? _value.encryptionKeyType
: encryptionKeyType // ignore: cast_nullable_to_non_nullable
as EncryptionKeyType,
biometricsEnabled: null == biometricsEnabled
? _value.biometricsEnabled
: biometricsEnabled // ignore: cast_nullable_to_non_nullable
as bool,
hiddenAccount: null == hiddenAccount
? _value.hiddenAccount
: hiddenAccount // ignore: cast_nullable_to_non_nullable
as bool,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LocalAccountImpl implements _LocalAccount {
const _$LocalAccountImpl(
{required this.identityMaster,
@Uint8ListJsonConverter() required this.identitySecretBytes,
required this.encryptionKeyType,
required this.biometricsEnabled,
required this.hiddenAccount,
required this.name});
factory _$LocalAccountImpl.fromJson(Map<String, dynamic> json) =>
_$$LocalAccountImplFromJson(json);
// The master key record for the account, containing the identityPublicKey
@override
final IdentityMaster identityMaster;
// The encrypted identity secret that goes with
// the identityPublicKey with appended salt
@override
@Uint8ListJsonConverter()
final Uint8List identitySecretBytes;
// The kind of encryption input used on the account
@override
final EncryptionKeyType encryptionKeyType;
// If account is not hidden, password can be retrieved via
@override
final bool biometricsEnabled;
// Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
@override
final bool hiddenAccount;
// Display name for account until it is unlocked
@override
final String name;
@override
String toString() {
return 'LocalAccount(identityMaster: $identityMaster, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LocalAccountImpl &&
(identical(other.identityMaster, identityMaster) ||
other.identityMaster == identityMaster) &&
const DeepCollectionEquality()
.equals(other.identitySecretBytes, identitySecretBytes) &&
(identical(other.encryptionKeyType, encryptionKeyType) ||
other.encryptionKeyType == encryptionKeyType) &&
(identical(other.biometricsEnabled, biometricsEnabled) ||
other.biometricsEnabled == biometricsEnabled) &&
(identical(other.hiddenAccount, hiddenAccount) ||
other.hiddenAccount == hiddenAccount) &&
(identical(other.name, name) || other.name == name));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
identityMaster,
const DeepCollectionEquality().hash(identitySecretBytes),
encryptionKeyType,
biometricsEnabled,
hiddenAccount,
name);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith =>
__$$LocalAccountImplCopyWithImpl<_$LocalAccountImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LocalAccountImplToJson(
this,
);
}
}
abstract class _LocalAccount implements LocalAccount {
const factory _LocalAccount(
{required final IdentityMaster identityMaster,
@Uint8ListJsonConverter() required final Uint8List identitySecretBytes,
required final EncryptionKeyType encryptionKeyType,
required final bool biometricsEnabled,
required final bool hiddenAccount,
required final String name}) = _$LocalAccountImpl;
factory _LocalAccount.fromJson(Map<String, dynamic> json) =
_$LocalAccountImpl.fromJson;
@override // The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster;
@override // The encrypted identity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes;
@override // The kind of encryption input used on the account
EncryptionKeyType get encryptionKeyType;
@override // If account is not hidden, password can be retrieved via
bool get biometricsEnabled;
@override // Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
bool get hiddenAccount;
@override // Display name for account until it is unlocked
String get name;
@override
@JsonKey(ignore: true)
_$$LocalAccountImplCopyWith<_$LocalAccountImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'local_account.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LocalAccountImpl _$$LocalAccountImplFromJson(Map<String, dynamic> json) =>
_$LocalAccountImpl(
identityMaster: IdentityMaster.fromJson(json['identity_master']),
identitySecretBytes: const Uint8ListJsonConverter()
.fromJson(json['identity_secret_bytes']),
encryptionKeyType:
EncryptionKeyType.fromJson(json['encryption_key_type']),
biometricsEnabled: json['biometrics_enabled'] as bool,
hiddenAccount: json['hidden_account'] as bool,
name: json['name'] as String,
);
Map<String, dynamic> _$$LocalAccountImplToJson(_$LocalAccountImpl instance) =>
<String, dynamic>{
'identity_master': instance.identityMaster.toJson(),
'identity_secret_bytes':
const Uint8ListJsonConverter().toJson(instance.identitySecretBytes),
'encryption_key_type': instance.encryptionKeyType.toJson(),
'biometrics_enabled': instance.biometricsEnabled,
'hidden_account': instance.hiddenAccount,
'name': instance.name,
};

View file

@ -0,0 +1,5 @@
class NewProfileSpec {
NewProfileSpec({required this.name, required this.pronouns});
String name;
String pronouns;
}

View file

@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:veilid_support/veilid_support.dart';
part 'user_login.freezed.dart';
part 'user_login.g.dart';
// Represents a currently logged in account
// User logins are stored in the user_logins tablestore table
// indexed by the accountMasterKey
@freezed
class UserLogin with _$UserLogin {
const factory UserLogin({
// Master record key for the user used to index the local accounts table
required TypedKey accountMasterRecordKey,
// The identity secret as unlocked from the local accounts table
required TypedSecret identitySecret,
// The account record key, owner key and secret pulled from the identity
required AccountRecordInfo accountRecordInfo,
// The time this login was most recently used
required Timestamp lastActive,
}) = _UserLogin;
factory UserLogin.fromJson(dynamic json) =>
_$UserLoginFromJson(json as Map<String, dynamic>);
}

View file

@ -0,0 +1,240 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_login.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
return _UserLogin.fromJson(json);
}
/// @nodoc
mixin _$UserLogin {
// Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey =>
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret =>
throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity
AccountRecordInfo get accountRecordInfo =>
throw _privateConstructorUsedError; // The time this login was most recently used
Timestamp get lastActive => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$UserLoginCopyWith<UserLogin> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserLoginCopyWith<$Res> {
factory $UserLoginCopyWith(UserLogin value, $Res Function(UserLogin) then) =
_$UserLoginCopyWithImpl<$Res, UserLogin>;
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
$AccountRecordInfoCopyWith<$Res> get accountRecordInfo;
}
/// @nodoc
class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
implements $UserLoginCopyWith<$Res> {
_$UserLoginCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_value.copyWith(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
: identitySecret // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
accountRecordInfo: null == accountRecordInfo
? _value.accountRecordInfo
: accountRecordInfo // ignore: cast_nullable_to_non_nullable
as AccountRecordInfo,
lastActive: null == lastActive
? _value.lastActive
: lastActive // ignore: cast_nullable_to_non_nullable
as Timestamp,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$AccountRecordInfoCopyWith<$Res> get accountRecordInfo {
return $AccountRecordInfoCopyWith<$Res>(_value.accountRecordInfo, (value) {
return _then(_value.copyWith(accountRecordInfo: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$UserLoginImplCopyWith<$Res>
implements $UserLoginCopyWith<$Res> {
factory _$$UserLoginImplCopyWith(
_$UserLoginImpl value, $Res Function(_$UserLoginImpl) then) =
__$$UserLoginImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
@override
$AccountRecordInfoCopyWith<$Res> get accountRecordInfo;
}
/// @nodoc
class __$$UserLoginImplCopyWithImpl<$Res>
extends _$UserLoginCopyWithImpl<$Res, _$UserLoginImpl>
implements _$$UserLoginImplCopyWith<$Res> {
__$$UserLoginImplCopyWithImpl(
_$UserLoginImpl _value, $Res Function(_$UserLoginImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_$UserLoginImpl(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
: identitySecret // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
accountRecordInfo: null == accountRecordInfo
? _value.accountRecordInfo
: accountRecordInfo // ignore: cast_nullable_to_non_nullable
as AccountRecordInfo,
lastActive: null == lastActive
? _value.lastActive
: lastActive // ignore: cast_nullable_to_non_nullable
as Timestamp,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserLoginImpl implements _UserLogin {
const _$UserLoginImpl(
{required this.accountMasterRecordKey,
required this.identitySecret,
required this.accountRecordInfo,
required this.lastActive});
factory _$UserLoginImpl.fromJson(Map<String, dynamic> json) =>
_$$UserLoginImplFromJson(json);
// Master record key for the user used to index the local accounts table
@override
final Typed<FixedEncodedString43> accountMasterRecordKey;
// The identity secret as unlocked from the local accounts table
@override
final Typed<FixedEncodedString43> identitySecret;
// The account record key, owner key and secret pulled from the identity
@override
final AccountRecordInfo accountRecordInfo;
// The time this login was most recently used
@override
final Timestamp lastActive;
@override
String toString() {
return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserLoginImpl &&
(identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
other.accountMasterRecordKey == accountMasterRecordKey) &&
(identical(other.identitySecret, identitySecret) ||
other.identitySecret == identitySecret) &&
(identical(other.accountRecordInfo, accountRecordInfo) ||
other.accountRecordInfo == accountRecordInfo) &&
(identical(other.lastActive, lastActive) ||
other.lastActive == lastActive));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountMasterRecordKey,
identitySecret, accountRecordInfo, lastActive);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith =>
__$$UserLoginImplCopyWithImpl<_$UserLoginImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserLoginImplToJson(
this,
);
}
}
abstract class _UserLogin implements UserLogin {
const factory _UserLogin(
{required final Typed<FixedEncodedString43> accountMasterRecordKey,
required final Typed<FixedEncodedString43> identitySecret,
required final AccountRecordInfo accountRecordInfo,
required final Timestamp lastActive}) = _$UserLoginImpl;
factory _UserLogin.fromJson(Map<String, dynamic> json) =
_$UserLoginImpl.fromJson;
@override // Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey;
@override // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret;
@override // The account record key, owner key and secret pulled from the identity
AccountRecordInfo get accountRecordInfo;
@override // The time this login was most recently used
Timestamp get lastActive;
@override
@JsonKey(ignore: true)
_$$UserLoginImplCopyWith<_$UserLoginImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_login.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserLoginImpl _$$UserLoginImplFromJson(Map<String, dynamic> json) =>
_$UserLoginImpl(
accountMasterRecordKey: Typed<FixedEncodedString43>.fromJson(
json['account_master_record_key']),
identitySecret:
Typed<FixedEncodedString43>.fromJson(json['identity_secret']),
accountRecordInfo:
AccountRecordInfo.fromJson(json['account_record_info']),
lastActive: Timestamp.fromJson(json['last_active']),
);
Map<String, dynamic> _$$UserLoginImplToJson(_$UserLoginImpl instance) =>
<String, dynamic>{
'account_master_record_key': instance.accountMasterRecordKey.toJson(),
'identity_secret': instance.identitySecret.toJson(),
'account_record_info': instance.accountRecordInfo.toJson(),
'last_active': instance.lastActive.toJson(),
};

View file

@ -0,0 +1 @@
export 'account_repository/account_repository.dart';

View file

@ -0,0 +1,136 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../components/default_app_bar.dart';
import '../../../components/signal_strength_meter.dart';
import '../../../entities/entities.dart';
import '../../../tools/tools.dart';
class NewAccountPage extends StatefulWidget {
const NewAccountPage({super.key});
@override
NewAccountPageState createState() => NewAccountPageState();
}
class NewAccountPageState extends State<NewAccountPage> {
final _formKey = GlobalKey<FormBuilderState>();
late bool isInAsyncCall = false;
static const String formFieldName = 'name';
static const String formFieldPronouns = 'pronouns';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
setState(() {});
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.portraitOnly);
});
}
Widget _newAccountForm(BuildContext context,
{required Future<void> Function(GlobalKey<FormBuilderState>)
onSubmit}) =>
FormBuilder(
key: _formKey,
child: ListView(
children: [
Text(translate('new_account_page.header'))
.textStyle(context.headlineSmall)
.paddingSymmetric(vertical: 16),
FormBuilderTextField(
autofocus: true,
name: formFieldName,
decoration:
InputDecoration(labelText: translate('account.form_name')),
maxLength: 64,
// The validator receives the text that the user has entered.
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
]),
),
FormBuilderTextField(
name: formFieldPronouns,
maxLength: 64,
decoration: InputDecoration(
labelText: translate('account.form_pronouns')),
),
Row(children: [
const Spacer(),
Text(translate('new_account_page.instructions'))
.toCenter()
.flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
isInAsyncCall = true;
});
try {
await onSubmit(_formKey);
} finally {
if (mounted) {
setState(() {
isInAsyncCall = false;
});
}
}
}
},
child: Text(translate('new_account_page.create')),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
@override
Widget build(BuildContext context) {
final displayModalHUD =
isInAsyncCall || !localAccounts.hasValue || !logins.hasValue;
return Scaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar(
title: Text(translate('new_account_page.titlebar')),
actions: [
const SignalStrengthMeterWidget(),
IconButton(
icon: const Icon(Icons.settings),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
context.go('/new_account/settings');
})
]),
body: _newAccountForm(
context,
onSubmit: (formKey) async {
FocusScope.of(context).unfocus();
try {
await createAccount();
} on Exception catch (e) {
if (context.mounted) {
await showErrorModal(context, translate('new_account_page.error'),
'Exception: $e');
}
}
},
).paddingSymmetric(horizontal: 24, vertical: 8),
).withModalHUD(context, displayModalHUD);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isInAsyncCall', isInAsyncCall));
}
}

View file

@ -0,0 +1 @@
export 'new_account_page/new_account_page.dart';