💄 Optimize auth devices
This commit is contained in:
@@ -447,6 +447,8 @@
|
|||||||
"lastActiveAt": "Last active at {}",
|
"lastActiveAt": "Last active at {}",
|
||||||
"authDeviceLogout": "Logout",
|
"authDeviceLogout": "Logout",
|
||||||
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
|
"authDeviceLogoutHint": "Are you sure you want to logout this device? This will also disable the push notification to this device.",
|
||||||
|
"authDeviceChallenges": "Device Usage",
|
||||||
|
"authDeviceHint": "Swipe left to edit label, swipe right to logout device.",
|
||||||
"typingHint": {
|
"typingHint": {
|
||||||
"one": "{} is typing...",
|
"one": "{} is typing...",
|
||||||
"other": "{} are typing..."
|
"other": "{} are typing..."
|
||||||
|
@@ -44,9 +44,12 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
|||||||
: 'failedToLoadUserInfoNetwork')
|
: 'failedToLoadUserInfoNetwork')
|
||||||
.tr()
|
.tr()
|
||||||
.trim(),
|
.trim(),
|
||||||
'${error.response?.statusCode ?? 'Network Error'}\n${error.response?.headers}',
|
'',
|
||||||
|
'${error.response?.statusCode ?? 'Network Error'}',
|
||||||
|
if (error.response?.headers != null) error.response?.headers,
|
||||||
|
if (error.response?.data != null)
|
||||||
jsonEncode(error.response?.data),
|
jsonEncode(error.response?.data),
|
||||||
].join('\n\n'),
|
].join('\n'),
|
||||||
iconStyle: IconStyle.error,
|
iconStyle: IconStyle.error,
|
||||||
neutralButtonTitle: 'retry'.tr(),
|
neutralButtonTitle: 'retry'.tr(),
|
||||||
negativeButtonTitle: 'okay'.tr(),
|
negativeButtonTitle: 'okay'.tr(),
|
||||||
|
@@ -6,10 +6,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/services/udid.dart';
|
import 'package:island/services/udid.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/sheet.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: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';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
@@ -43,32 +45,11 @@ class _DeviceListTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ExpansionTile(
|
||||||
isThreeLine: true,
|
title: Row(
|
||||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
spacing: 8,
|
||||||
leading: Icon(switch (device.platform) {
|
|
||||||
0 => Icons.device_unknown, // Unidentified
|
|
||||||
1 => Icons.web, // Web
|
|
||||||
2 => Icons.phone_iphone, // iOS
|
|
||||||
3 => Icons.phone_android, // Android
|
|
||||||
4 => Icons.laptop_mac, // macOS
|
|
||||||
5 => Icons.window, // Windows
|
|
||||||
6 => Icons.computer, // Linux
|
|
||||||
_ => Icons.device_unknown, // fallback
|
|
||||||
}).padding(top: 4),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Flexible(child: Text(device.deviceLabel ?? device.deviceName)),
|
||||||
'lastActiveAt'.tr(
|
|
||||||
args: [
|
|
||||||
DateFormat().format(
|
|
||||||
device.challenges.first.createdAt.toLocal(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(device.challenges.first.ipAddress),
|
|
||||||
if (device.isCurrent)
|
if (device.isCurrent)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -82,10 +63,29 @@ class _DeviceListTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(top: 4),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
title: Text(device.deviceLabel ?? device.deviceName),
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'lastActiveAt'.tr(
|
||||||
|
args: [device.challenges.first.createdAt.formatSystem()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: Icon(switch (device.platform) {
|
||||||
|
0 => Icons.device_unknown, // Unidentified
|
||||||
|
1 => Icons.web, // Web
|
||||||
|
2 => Icons.phone_iphone, // iOS
|
||||||
|
3 => Icons.phone_android, // Android
|
||||||
|
4 => Icons.laptop_mac, // macOS
|
||||||
|
5 => Icons.window, // Windows
|
||||||
|
6 => Icons.computer, // Linux
|
||||||
|
_ => Icons.device_unknown, // fallback
|
||||||
|
}).padding(top: 4),
|
||||||
trailing:
|
trailing:
|
||||||
isWideScreen(context)
|
isWideScreen(context)
|
||||||
? Row(
|
? Row(
|
||||||
@@ -105,6 +105,36 @@ class _DeviceListTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Text('authDeviceChallenges'.tr()),
|
||||||
|
),
|
||||||
|
for (final challenge in device.challenges)
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
title: Text(DateFormat().format(challenge.createdAt.toLocal())),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(challenge.ipAddress),
|
||||||
|
if (challenge.location != null)
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children:
|
||||||
|
[challenge.location?.city, challenge.location?.country]
|
||||||
|
.where((e) => e?.isNotEmpty ?? false)
|
||||||
|
.map((e) => Text(e!))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,11 +206,38 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: 'authSessions'.tr(),
|
titleText: 'authSessions'.tr(),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (!wideScreen)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.info, size: 16).padding(top: 2),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'authDeviceHint'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
child: authDevices.when(
|
child: authDevices.when(
|
||||||
data:
|
data:
|
||||||
(data) => ExtendedRefreshIndicator(
|
(data) => ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() => Future.sync(() => ref.invalidate(authDevicesProvider)),
|
() => Future.sync(
|
||||||
|
() => ref.invalidate(authDevicesProvider),
|
||||||
|
),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
itemCount: data.length,
|
itemCount: data.length,
|
||||||
@@ -221,9 +278,23 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
'authDeviceLogout'.tr(),
|
'authDeviceLogout'.tr(),
|
||||||
);
|
);
|
||||||
if (confirm && context.mounted) {
|
if (confirm && context.mounted) {
|
||||||
logoutDevice(device.deviceId);
|
try {
|
||||||
|
showLoadingModal(context);
|
||||||
|
final apiClient = ref.watch(
|
||||||
|
apiClientProvider,
|
||||||
|
);
|
||||||
|
await apiClient.delete(
|
||||||
|
'/id/accounts/me/devices/${device.deviceId}',
|
||||||
|
);
|
||||||
|
ref.invalidate(authDevicesProvider);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
} finally {
|
||||||
|
if (context.mounted)
|
||||||
|
hideLoadingModal(context);
|
||||||
}
|
}
|
||||||
return false; // Don't dismiss
|
}
|
||||||
|
return confirm;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: _DeviceListTile(
|
child: _DeviceListTile(
|
||||||
@@ -243,6 +314,9 @@ class AccountSessionSheet extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
loading: () => ResponseLoadingWidget(),
|
loading: () => ResponseLoadingWidget(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user