diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 83759cc9..37d6dd53 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -922,5 +922,9 @@ "deleteSecret": "Delete Secret", "deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.", "generateSecret": "Generate New Secret", - "created_at": "Created at {}" + "createdAt": "Created at {}", + "newSecretGenerated": "New Secret Generated", + "copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.", + "expiresIn": "Expires In (seconds)", + "isOidc": "OIDC Compliant" } \ No newline at end of file diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 091c1cac..8b82c796 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -851,5 +851,9 @@ "deleteSecret": "删除密钥", "deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。", "generateSecret": "生成新密钥", - "created_at": "创建于 {}" + "createdAt": "创建于 {}", + "newSecretGenerated": "已生成新密钥", + "copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。", + "expiresIn": "过期时间(秒)", + "isOidc": "OIDC 兼容" } diff --git a/assets/i18n/zh-TW.json b/assets/i18n/zh-TW.json index 49963ba3..d26d46b6 100644 --- a/assets/i18n/zh-TW.json +++ b/assets/i18n/zh-TW.json @@ -819,5 +819,9 @@ "deleteSecret": "刪除密鑰", "deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。", "generateSecret": "產生新密鑰", - "created_at": "建立於 {}" + "createdAt": "建立於 {}", + "newSecretGenerated": "已產生新密鑰", + "copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。", + "expiresIn": "過期時間(秒)", + "isOidc": "OIDC 相容" } \ No newline at end of file diff --git a/lib/models/custom_app_secret.dart b/lib/models/custom_app_secret.dart index 6a8f052e..c02fceb1 100644 --- a/lib/models/custom_app_secret.dart +++ b/lib/models/custom_app_secret.dart @@ -7,9 +7,11 @@ part 'custom_app_secret.g.dart'; sealed class CustomAppSecret with _$CustomAppSecret { const factory CustomAppSecret({ required String id, - required String secret, + required String? secret, required DateTime createdAt, String? description, + int? expiresIn, + bool? isOidc, }) = _CustomAppSecret; factory CustomAppSecret.fromJson(Map json) => diff --git a/lib/models/custom_app_secret.freezed.dart b/lib/models/custom_app_secret.freezed.dart index 9e7f6771..f78faac3 100644 --- a/lib/models/custom_app_secret.freezed.dart +++ b/lib/models/custom_app_secret.freezed.dart @@ -15,7 +15,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$CustomAppSecret { - String get id; String get secret; DateTime get createdAt; String? get description; + String get id; String? get secret; DateTime get createdAt; String? get description; int? get expiresIn; bool? get isOidc; /// Create a copy of CustomAppSecret /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -28,16 +28,16 @@ $CustomAppSecretCopyWith get copyWith => _$CustomAppSecretCopyW @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); +int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc); @override String toString() { - return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; + return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)'; } @@ -48,7 +48,7 @@ abstract mixin class $CustomAppSecretCopyWith<$Res> { factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl; @useResult $Res call({ - String id, String secret, DateTime createdAt, String? description + String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc }); @@ -65,13 +65,15 @@ class _$CustomAppSecretCopyWithImpl<$Res> /// Create a copy of CustomAppSecret /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = null,Object? createdAt = null,Object? description = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable -as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable +as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?, +as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable +as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable +as bool?, )); } @@ -118,10 +120,7 @@ return $default(_that);case _: final _that = this; switch (_that) { case _CustomAppSecret(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} +return $default(_that);} } /// A variant of `map` that fallback to returning `null`. /// @@ -156,10 +155,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String secret, DateTime createdAt, String? description)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _CustomAppSecret() when $default != null: -return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: +return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _: return orElse(); } @@ -177,13 +176,10 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String secret, DateTime createdAt, String? description) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc) $default,) {final _that = this; switch (_that) { case _CustomAppSecret(): -return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: - throw StateError('Unexpected subclass'); - -} +return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);} } /// A variant of `when` that fallback to returning `null` /// @@ -197,10 +193,10 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String secret, DateTime createdAt, String? description)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc)? $default,) {final _that = this; switch (_that) { case _CustomAppSecret() when $default != null: -return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: +return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _: return null; } @@ -212,13 +208,15 @@ return $default(_that.id,_that.secret,_that.createdAt,_that.description);case _: @JsonSerializable() class _CustomAppSecret implements CustomAppSecret { - const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description}); + const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description, this.expiresIn, this.isOidc}); factory _CustomAppSecret.fromJson(Map json) => _$CustomAppSecretFromJson(json); @override final String id; -@override final String secret; +@override final String? secret; @override final DateTime createdAt; @override final String? description; +@override final int? expiresIn; +@override final bool? isOidc; /// Create a copy of CustomAppSecret /// with the given fields replaced by the non-null parameter values. @@ -233,16 +231,16 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)); } @JsonKey(includeFromJson: false, includeToJson: false) @override -int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description); +int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc); @override String toString() { - return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description)'; + return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)'; } @@ -253,7 +251,7 @@ abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecret factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl; @override @useResult $Res call({ - String id, String secret, DateTime createdAt, String? description + String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc }); @@ -270,13 +268,15 @@ class __$CustomAppSecretCopyWithImpl<$Res> /// Create a copy of CustomAppSecret /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = null,Object? createdAt = null,Object? description = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) { return _then(_CustomAppSecret( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable -as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable +as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable +as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?, +as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable +as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable +as bool?, )); } diff --git a/lib/models/custom_app_secret.g.dart b/lib/models/custom_app_secret.g.dart index daf15170..a2d56eb1 100644 --- a/lib/models/custom_app_secret.g.dart +++ b/lib/models/custom_app_secret.g.dart @@ -9,9 +9,11 @@ part of 'custom_app_secret.dart'; _CustomAppSecret _$CustomAppSecretFromJson(Map json) => _CustomAppSecret( id: json['id'] as String, - secret: json['secret'] as String, + secret: json['secret'] as String?, createdAt: DateTime.parse(json['created_at'] as String), description: json['description'] as String?, + expiresIn: (json['expires_in'] as num?)?.toInt(), + isOidc: json['is_oidc'] as bool?, ); Map _$CustomAppSecretToJson(_CustomAppSecret instance) => @@ -20,4 +22,6 @@ Map _$CustomAppSecretToJson(_CustomAppSecret instance) => 'secret': instance.secret, 'created_at': instance.createdAt.toIso8601String(), 'description': instance.description, + 'expires_in': instance.expiresIn, + 'is_oidc': instance.isOidc, }; diff --git a/lib/screens/developers/app_secrets.dart b/lib/screens/developers/app_secrets.dart index 17d9ae28..cd3fa5ae 100644 --- a/lib/screens/developers/app_secrets.dart +++ b/lib/screens/developers/app_secrets.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/custom_app_secret.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -46,24 +48,127 @@ class AppSecretsScreen extends HookConsumerWidget { customAppSecretsProvider(publisherName, projectId, appId), ); - Future generateSecret() async { - final client = ref.read(apiClientProvider); - try { - showLoadingModal(context); - await client - .post( - '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', - ) - .then((_) { - ref.invalidate( - customAppSecretsProvider(publisherName, projectId, appId), + void showNewSecretSheet(String newSecret) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: + (context) => SheetScaffold( + titleText: 'newSecretGenerated'.tr(), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Text('copySecretHint'.tr()), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), + ), + child: SelectableText(newSecret), + ), + const SizedBox(height: 20), + FilledButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: newSecret)); + }, + icon: const Icon(Symbols.copy_all), + label: Text('copy'.tr()), + ), + ], + ), + ), + ), + ).whenComplete(() { + ref.invalidate( + customAppSecretsProvider(publisherName, projectId, appId), + ); + }); + } + + void createSecret() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) { + return HookBuilder( + builder: (context) { + final descriptionController = useTextEditingController(); + final expiresInController = useTextEditingController(); + final isOidc = useState(false); + + return SheetScaffold( + titleText: 'generateSecret'.tr(), + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: descriptionController, + decoration: InputDecoration( + labelText: 'description'.tr(), + ), + autofocus: true, + ), + const SizedBox(height: 20), + TextFormField( + controller: expiresInController, + decoration: InputDecoration( + labelText: 'expiresIn'.tr(), + ), + keyboardType: TextInputType.number, + ), + const SizedBox(height: 20), + SwitchListTile( + title: Text('isOidc'.tr()), + value: isOidc.value, + onChanged: (value) => isOidc.value = value, + ), + const SizedBox(height: 20), + FilledButton.icon( + onPressed: () async { + final description = descriptionController.text; + final expiresIn = int.tryParse( + expiresInController.text, + ); + Navigator.pop(context); // Close the sheet + try { + final client = ref.read(apiClientProvider); + final resp = await client.post( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', + data: { + 'description': description, + 'expires_in': expiresIn, + 'is_oidc': isOidc.value, + }, + ); + final newSecret = CustomAppSecret.fromJson( + resp.data, + ); + if (newSecret.secret != null) { + showNewSecretSheet(newSecret.secret!); + } + } catch (e) { + showErrorAlert(e.toString()); + } + }, + icon: const Icon(Symbols.add), + label: Text('create'.tr()), + ), + ], + ), + ), ); - }); - } catch (err) { - showErrorAlert(err); - } finally { - if (context.mounted) hideLoadingModal(context); - } + }, + ); + }, + ); } return secrets.when( @@ -81,9 +186,8 @@ class AppSecretsScreen extends HookConsumerWidget { children: [ ListTile( leading: const Icon(Symbols.add), - trailing: const Icon(Symbols.chevron_right), - title: Text('appSecretsGenerate').tr(), - onTap: generateSecret, + title: Text('generateSecret'.tr()), + onTap: createSecret, ), Expanded( child: ListView.builder( @@ -91,9 +195,9 @@ class AppSecretsScreen extends HookConsumerWidget { itemBuilder: (context, index) { final secret = data[index]; return ListTile( - title: Text(secret.id), + title: Text(secret.description ?? secret.id), subtitle: Text( - 'created_at'.tr( + 'createdAt'.tr( args: [secret.createdAt.toIso8601String()], ), ), @@ -104,7 +208,7 @@ class AppSecretsScreen extends HookConsumerWidget { icon: const Icon(Symbols.copy_all), onPressed: () { Clipboard.setData( - ClipboardData(text: secret.secret), + ClipboardData(text: secret.secret!), ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('secretCopied'.tr())), @@ -120,19 +224,16 @@ class AppSecretsScreen extends HookConsumerWidget { ).then((confirm) { if (confirm) { final client = ref.read(apiClientProvider); - client - .delete( - '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}', - ) - .then((_) { - ref.invalidate( - customAppSecretsProvider( - publisherName, - projectId, - appId, - ), - ); - }); + client.delete( + '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}', + ); + ref.invalidate( + customAppSecretsProvider( + publisherName, + projectId, + appId, + ), + ); } }); },