✨ Connection management
This commit is contained in:
parent
7f26196e85
commit
00b3dc7be6
@ -128,6 +128,24 @@
|
|||||||
"connectionConnected": "Connected",
|
"connectionConnected": "Connected",
|
||||||
"connectionDisconnected": "Disconnected",
|
"connectionDisconnected": "Disconnected",
|
||||||
"connectionReconnecting": "Reconnecting",
|
"connectionReconnecting": "Reconnecting",
|
||||||
|
"accountConnections": "Account Connections",
|
||||||
|
"accountConnectionsDescription": "Manage your external account connections",
|
||||||
|
"accountConnectionAdd": "Add Connection",
|
||||||
|
"accountConnectionDelete": "Delete Connection",
|
||||||
|
"accountConnectionDeleteHint": "Are you sure you want to delete this connection? This action cannot be undone.",
|
||||||
|
"accountConnectionsEmpty": "No connections found. Add a connection to get started.",
|
||||||
|
"accountConnectionProvider": "Provider",
|
||||||
|
"accountConnectionProviderHint": "Enter provider name",
|
||||||
|
"accountConnectionIdentifier": "Identifier",
|
||||||
|
"accountConnectionIdentifierHint": "Enter your identifier for this provider",
|
||||||
|
"accountConnectionDescription": "Add a connection to link your account with external services.",
|
||||||
|
"accountConnectionAddSuccess": "Connection added successfully.",
|
||||||
|
"accountConnectionAddError": "Unable to setup connection.",
|
||||||
|
"accountConnectionProviderApple": "Apple",
|
||||||
|
"accountConnectionProviderMicrosoft": "Microsoft",
|
||||||
|
"accountConnectionProviderGoogle": "Google",
|
||||||
|
"accountConnectionProviderGithub": "GitHub",
|
||||||
|
"accountConnectionProviderDiscord": "Discord",
|
||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:auto_route/annotations.dart';
|
import 'package:auto_route/annotations.dart';
|
||||||
@ -6,15 +5,13 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/auth.dart';
|
import 'package:island/models/auth.dart';
|
||||||
import 'package:island/models/user.dart';
|
import 'package:island/models/user.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/pods/userinfo.dart';
|
import 'package:island/pods/userinfo.dart';
|
||||||
import 'package:island/screens/account/me/settings_auth_factors.dart';
|
import 'package:island/screens/account/me/settings_auth_factors.dart';
|
||||||
|
import 'package:island/screens/account/me/settings_connections.dart';
|
||||||
import 'package:island/screens/account/me/settings_contacts.dart';
|
import 'package:island/screens/account/me/settings_contacts.dart';
|
||||||
import 'package:island/screens/auth/captcha.dart';
|
import 'package:island/screens/auth/captcha.dart';
|
||||||
import 'package:island/screens/auth/login.dart';
|
import 'package:island/screens/auth/login.dart';
|
||||||
@ -22,10 +19,8 @@ import 'package:island/services/responsive.dart';
|
|||||||
import 'package:island/widgets/account/account_session_sheet.dart';
|
import 'package:island/widgets/account/account_session_sheet.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/sheet.dart';
|
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
@ -47,6 +42,15 @@ Future<List<SnContactMethod>> contactMethods(Ref ref) async {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<List<SnAccountConnection>> accountConnections(Ref ref) async {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
final resp = await client.get('/accounts/me/connections');
|
||||||
|
return resp.data
|
||||||
|
.map<SnAccountConnection>((e) => SnAccountConnection.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class AccountSettingsScreen extends HookConsumerWidget {
|
class AccountSettingsScreen extends HookConsumerWidget {
|
||||||
const AccountSettingsScreen({super.key});
|
const AccountSettingsScreen({super.key});
|
||||||
@ -124,6 +128,104 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ExpansionTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Symbols.link,
|
||||||
|
).alignment(Alignment.centerLeft).width(48),
|
||||||
|
title: Text('accountConnections').tr(),
|
||||||
|
subtitle: Text('accountConnectionsDescription').tr().fontSize(12),
|
||||||
|
tilePadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
children: [
|
||||||
|
ref
|
||||||
|
.watch(accountConnectionsProvider)
|
||||||
|
.when(
|
||||||
|
data:
|
||||||
|
(connections) => Column(
|
||||||
|
children: [
|
||||||
|
for (final connection in connections)
|
||||||
|
ListTile(
|
||||||
|
minLeadingWidth: 48,
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 16,
|
||||||
|
right: 17,
|
||||||
|
top: 2,
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
Text(
|
||||||
|
getLocalizedProviderName(connection.provider),
|
||||||
|
).tr(),
|
||||||
|
subtitle:
|
||||||
|
connection.meta.isNotEmpty
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (final meta
|
||||||
|
in connection.meta.entries)
|
||||||
|
Text(
|
||||||
|
'${meta.key.split('_').map((word) => word[0].toUpperCase() + word.substring(1)).join(' ')}: ${meta.value}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(connection.providedIdentifier),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child: Icon(getProviderIcon(connection.provider)),
|
||||||
|
).padding(top: 4),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
isThreeLine: true,
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AccountConnectionSheet(
|
||||||
|
connection: connection,
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
ref.invalidate(accountConnectionsProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (connections.isNotEmpty) const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
minLeadingWidth: 48,
|
||||||
|
contentPadding: const EdgeInsets.only(
|
||||||
|
left: 24,
|
||||||
|
right: 17,
|
||||||
|
),
|
||||||
|
title: Text('accountConnectionAdd').tr(),
|
||||||
|
leading: const Icon(Symbols.add),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) =>
|
||||||
|
const AccountConnectionNewSheet(),
|
||||||
|
).then((value) {
|
||||||
|
if (value == true) {
|
||||||
|
ref.invalidate(accountConnectionsProvider);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(err, _) => ResponseErrorWidget(
|
||||||
|
error: err,
|
||||||
|
onRetry: () => ref.invalidate(accountConnectionsProvider),
|
||||||
|
),
|
||||||
|
loading: () => const ResponseLoadingWidget(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
ExpansionTile(
|
ExpansionTile(
|
||||||
leading: const Icon(
|
leading: const Icon(
|
||||||
Symbols.security,
|
Symbols.security,
|
||||||
|
@ -44,5 +44,26 @@ final contactMethodsProvider =
|
|||||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef ContactMethodsRef = AutoDisposeFutureProviderRef<List<SnContactMethod>>;
|
typedef ContactMethodsRef = AutoDisposeFutureProviderRef<List<SnContactMethod>>;
|
||||||
|
String _$accountConnectionsHash() =>
|
||||||
|
r'38a309d596e0ea2539cd92ea86984e1e4fb346e4';
|
||||||
|
|
||||||
|
/// See also [accountConnections].
|
||||||
|
@ProviderFor(accountConnections)
|
||||||
|
final accountConnectionsProvider =
|
||||||
|
AutoDisposeFutureProvider<List<SnAccountConnection>>.internal(
|
||||||
|
accountConnections,
|
||||||
|
name: r'accountConnectionsProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$accountConnectionsHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef AccountConnectionsRef =
|
||||||
|
AutoDisposeFutureProviderRef<List<SnAccountConnection>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
339
lib/screens/account/me/settings_connections.dart
Normal file
339
lib/screens/account/me/settings_connections.dart
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/models/auth.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/account/me/settings.dart';
|
||||||
|
import 'package:island/services/time.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:sign_in_with_apple/sign_in_with_apple.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
// Helper function to get provider icon and localized name
|
||||||
|
IconData getProviderIcon(String provider) {
|
||||||
|
switch (provider.toLowerCase()) {
|
||||||
|
case 'apple':
|
||||||
|
return Icons.apple;
|
||||||
|
case 'microsoft':
|
||||||
|
return Symbols.window; // Microsoft icon alternative
|
||||||
|
case 'google':
|
||||||
|
return Symbols.g_translate; // Google icon alternative
|
||||||
|
case 'github':
|
||||||
|
return Symbols.code; // GitHub icon
|
||||||
|
case 'discord':
|
||||||
|
return Symbols.forum; // Discord icon alternative
|
||||||
|
default:
|
||||||
|
return Symbols.link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLocalizedProviderName(String provider) {
|
||||||
|
switch (provider.toLowerCase()) {
|
||||||
|
case 'apple':
|
||||||
|
return 'accountConnectionProviderApple'.tr();
|
||||||
|
case 'microsoft':
|
||||||
|
return 'accountConnectionProviderMicrosoft'.tr();
|
||||||
|
case 'google':
|
||||||
|
return 'accountConnectionProviderGoogle'.tr();
|
||||||
|
case 'github':
|
||||||
|
return 'accountConnectionProviderGithub'.tr();
|
||||||
|
case 'discord':
|
||||||
|
return 'accountConnectionProviderDiscord'.tr();
|
||||||
|
default:
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountConnectionSheet extends HookConsumerWidget {
|
||||||
|
final SnAccountConnection connection;
|
||||||
|
const AccountConnectionSheet({super.key, required this.connection});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
Future<void> deleteConnection() async {
|
||||||
|
final confirm = await showConfirmAlert(
|
||||||
|
'accountConnectionDeleteHint'.tr(),
|
||||||
|
'accountConnectionDelete'.tr(),
|
||||||
|
);
|
||||||
|
if (!confirm || !context.mounted) return;
|
||||||
|
try {
|
||||||
|
showLoadingModal(context);
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete('/accounts/me/connections/${connection.id}');
|
||||||
|
if (context.mounted) Navigator.pop(context, true);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'accountConnections'.tr(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(getProviderIcon(connection.provider), size: 32),
|
||||||
|
const Gap(8),
|
||||||
|
Text(getLocalizedProviderName(connection.provider)).tr(),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
connection.providedIdentifier,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text(
|
||||||
|
connection.lastUsedAt.formatSystem(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
).opacity(0.85),
|
||||||
|
],
|
||||||
|
).padding(all: 20),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.delete),
|
||||||
|
title: Text('accountConnectionDelete').tr(),
|
||||||
|
onTap: deleteConnection,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||||
|
const AccountConnectionNewSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final selectedProvider = useState<String>('apple');
|
||||||
|
|
||||||
|
// List of available providers
|
||||||
|
final providers = ['apple', 'microsoft', 'google', 'github', 'discord'];
|
||||||
|
|
||||||
|
Future<void> addConnection() async {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|
||||||
|
switch (selectedProvider.value.toLowerCase()) {
|
||||||
|
case 'apple':
|
||||||
|
try {
|
||||||
|
final credential = await SignInWithApple.getAppleIDCredential(
|
||||||
|
scopes: [AppleIDAuthorizationScopes.email],
|
||||||
|
webAuthenticationOptions: WebAuthenticationOptions(
|
||||||
|
clientId: 'dev.solsynth.solarpass',
|
||||||
|
redirectUri: Uri.parse(
|
||||||
|
'https://nt.solian.app/auth/callback/apple',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) showLoadingModal(context);
|
||||||
|
|
||||||
|
await client.post(
|
||||||
|
'/auth/connect/apple/mobile',
|
||||||
|
data: {
|
||||||
|
'identity_token': credential.identityToken!,
|
||||||
|
'authorization_code': credential.authorizationCode,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
showSnackBar(context, 'accountConnectionAddSuccess'.tr());
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err is SignInWithAppleCredentialsException) return;
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted) hideLoadingModal(context);
|
||||||
|
}
|
||||||
|
case 'microsoft':
|
||||||
|
case 'google':
|
||||||
|
case 'github':
|
||||||
|
case 'discord':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
showSnackBar(context, 'accountConnectionAddError'.tr());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'accountConnectionAdd'.tr(),
|
||||||
|
child: Column(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: selectedProvider.value,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: Icon(getProviderIcon(selectedProvider.value)),
|
||||||
|
labelText: 'accountConnectionProvider'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items:
|
||||||
|
providers.map((String provider) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: provider,
|
||||||
|
child: Row(
|
||||||
|
children: [Text(getLocalizedProviderName(provider)).tr()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (String? newValue) {
|
||||||
|
if (newValue != null) {
|
||||||
|
selectedProvider.value = newValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Text('accountConnectionDescription'.tr()),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: addConnection,
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
label: Text('next').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 20, vertical: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountConnectionsSheet extends HookConsumerWidget {
|
||||||
|
const AccountConnectionsSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final connections = ref.watch(accountConnectionsProvider);
|
||||||
|
|
||||||
|
return SheetScaffold(
|
||||||
|
titleText: 'accountConnections'.tr(),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await showModalBottomSheet<bool>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => const AccountConnectionNewSheet(),
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
ref.invalidate(accountConnectionsProvider);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: connections.when(
|
||||||
|
data:
|
||||||
|
(data) => RefreshIndicator(
|
||||||
|
onRefresh:
|
||||||
|
() => Future.sync(
|
||||||
|
() => ref.invalidate(accountConnectionsProvider),
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
data.isEmpty
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
'accountConnectionsEmpty'.tr(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).padding(horizontal: 32),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final connection = data[index];
|
||||||
|
return Dismissible(
|
||||||
|
key: Key('connection-${connection.id}'),
|
||||||
|
direction: DismissDirection.endToStart,
|
||||||
|
background: Container(
|
||||||
|
color: Colors.red,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
confirmDismiss: (direction) async {
|
||||||
|
final confirm = await showConfirmAlert(
|
||||||
|
'accountConnectionDeleteHint'.tr(),
|
||||||
|
'accountConnectionDelete'.tr(),
|
||||||
|
);
|
||||||
|
if (confirm && context.mounted) {
|
||||||
|
try {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/accounts/me/connections/${connection.id}',
|
||||||
|
);
|
||||||
|
ref.invalidate(accountConnectionsProvider);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
getProviderIcon(connection.provider),
|
||||||
|
),
|
||||||
|
title:
|
||||||
|
Text(
|
||||||
|
getLocalizedProviderName(
|
||||||
|
connection.provider,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
subtitle: Text(connection.providedIdentifier),
|
||||||
|
trailing: Text(
|
||||||
|
DateFormat.yMd().format(
|
||||||
|
connection.lastUsedAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
final result = await showModalBottomSheet<bool>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => AccountConnectionSheet(
|
||||||
|
connection: connection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result == true) {
|
||||||
|
ref.invalidate(accountConnectionsProvider);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error:
|
||||||
|
(err, _) => ResponseErrorWidget(
|
||||||
|
error: err,
|
||||||
|
onRetry: () => ref.invalidate(accountConnectionsProvider),
|
||||||
|
),
|
||||||
|
loading: () => const ResponseLoadingWidget(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user