diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart index 31ce730..4404f7f 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart @@ -57,6 +57,16 @@ abstract interface class IAuthService { required String? walletPassword, }); + /// Method to store custom metadata for the user. + /// + /// Overwrites any existing metadata. + /// + /// This does not emit an auth state change event. + /// + /// NB: This is intended to only be a short-term solution until the SDK + /// is fully integrated with KW. This may be deprecated in the future. + Future setActiveUserMetadata(JsonMap metadata); + Stream get authStateChanges; void dispose(); } @@ -296,4 +306,28 @@ class KdfAuthService implements IAuthService { return result == null; }); } + + /// Returns the [KdfUser] associated with the active wallet if authenticated, + /// otherwise throws an [AuthException]. + Future _activeUserOrThrow() async { + final activeUser = await getActiveUser(); + if (activeUser == null) { + throw AuthException.notSignedIn(); + } + return activeUser; + } + + @override + Future setActiveUserMetadata( + Map metadata, + ) async { + final activeUser = await _activeUserOrThrow(); + // TODO: Implement locks for this to avoid this method interfering with + // more sensitive operations. + final user = await _secureStorage.getUser(activeUser.walletId.name); + if (user == null) throw AuthException.notFound(); + + final updatedUser = user.copyWith(metadata: metadata); + await _secureStorage.saveUser(updatedUser); + } } diff --git a/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart b/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart index bc48b59..cb0d7df 100644 --- a/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart +++ b/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart @@ -94,6 +94,45 @@ abstract interface class KomodoDefiAuth { /// is signed in. Future getMnemonicPlainText(String walletPassword); + /// Sets the value of a single key in the active user's metadata. + /// + /// This preserves any existing metadata, and overwrites the value only for + /// the specified key. + /// + /// Throws an exception if there is no active user. + /// + /// Setting a value to `null` will remove the key from the metadata. + /// + /// This does not emit an auth state change event. + /// + /// + /// NB: This is intended to only be a short-term solution until the SDK + /// is fully integrated with KW. This may be deprecated in the future. + /// + /// Example: + /// final _komodoDefiSdk = KomodoDefiSdk.global; + /// + /// await _komodoDefiSdk.auth.setOrRemoveActiveUserKeyValue( + /// 'custom_tokens', + /// { + /// 'tokens': [ + /// { + /// 'foo': 'bar', + /// 'name': 'Foo Token', + // / 'symbol': 'FOO', + /// // ... + /// } + // / ], + /// }.toJsonString(), + /// ); + /// final tokenJson = (await _komodoDefiSdk.auth.currentUser) + /// ?.metadata + /// .valueOrNull('custom_tokens', 'tokens'); + /// + /// print('Custom tokens: $tokenJson'); + + Future setOrRemoveActiveUserKeyValue(String key, dynamic value); + /// Disposes of any resources held by the authentication service. /// /// This method should be called when the authentication service is no longer @@ -211,11 +250,6 @@ class KomodoDefiLocalAuth implements KomodoDefiAuth { return user; } - // // Retrieve AuthOptions by wallet name when needed - // Future getAuthOptions(String walletName) async { - // return _secureStorage.getAuthOptions(walletName); - // } - @override Stream get authStateChanges async* { await ensureInitialized(); @@ -298,6 +332,22 @@ class KomodoDefiLocalAuth implements KomodoDefiAuth { } } + @override + Future setOrRemoveActiveUserKeyValue( + String key, + dynamic value, + ) async { + final activeUser = await _authService.getActiveUser(); + + if (activeUser == null) throw AuthException.notFound(); + + final updatedMetadata = JsonMap.from(activeUser.metadata)..[key] = value; + + if (value == null) updatedMetadata.remove(key); + + await _authService.setActiveUserMetadata(updatedMetadata); + } + Future _assertAuthState(bool expected) async { await ensureInitialized(); final signedIn = await isSignedIn(); diff --git a/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart b/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart index 5e9a9f0..f76db35 100644 --- a/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart +++ b/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart @@ -18,6 +18,12 @@ class AuthException implements Exception { this.details = const {}, }); + // Common exception constructors convenience methods + AuthException.notSignedIn() + : this('Not signed in', type: AuthExceptionType.unauthorized); + AuthException.notFound() + : this('Not found', type: AuthExceptionType.walletNotFound); + /// The error message. final String message; diff --git a/packages/komodo_defi_types/lib/src/auth/kdf_user.dart b/packages/komodo_defi_types/lib/src/auth/kdf_user.dart index 0286ff8..538f70a 100644 --- a/packages/komodo_defi_types/lib/src/auth/kdf_user.dart +++ b/packages/komodo_defi_types/lib/src/auth/kdf_user.dart @@ -55,6 +55,7 @@ class KdfUser extends Equatable { required this.walletId, required this.authOptions, required this.isBip39Seed, + this.metadata = const {}, }); /// Create from JSON representation @@ -62,11 +63,13 @@ class KdfUser extends Equatable { walletId: WalletId.fromJson(json.value('wallet_id')), authOptions: AuthOptions.fromJson(json.value('auth_options')), isBip39Seed: json.value('is_bip39_seed'), + metadata: json.valueOrNull('metadata') ?? const {}, ); final WalletId walletId; final AuthOptions authOptions; final bool isBip39Seed; + final JsonMap metadata; bool get isHd => authOptions.derivationMethod == DerivationMethod.hdWallet; @@ -75,20 +78,23 @@ class KdfUser extends Equatable { WalletId? walletId, AuthOptions? authOptions, bool? isBip39Seed, + JsonMap? metadata, }) { return KdfUser( walletId: walletId ?? this.walletId, authOptions: authOptions ?? this.authOptions, isBip39Seed: isBip39Seed ?? this.isBip39Seed, + metadata: metadata ?? this.metadata, ); } @override - List get props => [walletId, authOptions, isBip39Seed]; + List get props => [walletId, authOptions, isBip39Seed, metadata]; JsonMap toJson() => { 'wallet_id': walletId.toJson(), 'auth_options': authOptions.toJson(), 'is_bip39_seed': isBip39Seed, + if (metadata.isNotEmpty) 'metadata': metadata, }; }