[Passkey] Add check status option + other fixes (#2123)
## Description ## Tests
This commit is contained in:
Submodule auth/flutter updated: ba39319843...761747bfc5
@@ -42,3 +42,7 @@ class InvalidStateError extends AssertionError {
|
||||
class SrpSetupNotCompleteError extends Error {}
|
||||
|
||||
class AuthenticatorKeyNotFound extends Error {}
|
||||
|
||||
class PassKeySessionNotVerifiedError extends Error {}
|
||||
|
||||
class PassKeySessionExpiredError extends Error {}
|
||||
|
||||
@@ -269,6 +269,7 @@
|
||||
"privacy": "Privacy",
|
||||
"terms": "Terms",
|
||||
"checkForUpdates": "Check for updates",
|
||||
"checkStatus": "Check status",
|
||||
"downloadUpdate": "Download",
|
||||
"criticalUpdateAvailable": "Critical update available",
|
||||
"updateAvailable": "Update available",
|
||||
@@ -417,6 +418,9 @@
|
||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||
"waitingForVerification": "Waiting for verification...",
|
||||
"passkey": "Passkey",
|
||||
"passKeyPendingVerification": "Verification is still pending",
|
||||
"loginSessionExpired" : "Session expired",
|
||||
"loginSessionExpiredDetails": "Your session has expired. Please login again.",
|
||||
"developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
|
||||
"developerSettings": "Developer settings",
|
||||
"serverEndpoint": "Server endpoint",
|
||||
|
||||
@@ -266,6 +266,31 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getTokenForPasskeySession(String sessionID) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
"${_config.getHttpEndpoint()}/users/two-factor/passkeys/get-token",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} on DioException catch (e) {
|
||||
if (e.response != null) {
|
||||
if (e.response!.statusCode == 404 || e.response!.statusCode == 410) {
|
||||
throw PassKeySessionExpiredError();
|
||||
}
|
||||
if (e.response!.statusCode == 400) {
|
||||
throw PassKeySessionNotVerifiedError();
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
_logger.severe("unexpected error", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPassKeyVerified(BuildContext context, Map response) async {
|
||||
final ProgressDialog dialog =
|
||||
createProgressDialog(context, context.l10n.pleaseWait);
|
||||
|
||||
@@ -33,7 +33,7 @@ enum ButtonType {
|
||||
|
||||
Color defaultButtonColor(EnteColorScheme colorScheme) {
|
||||
if (isPrimary) {
|
||||
return colorScheme.primary500;
|
||||
return colorScheme.primary400;
|
||||
}
|
||||
if (isSecondary) {
|
||||
return colorScheme.fillFaint;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/account/two_factor.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
@@ -50,6 +51,30 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkStatus() async {
|
||||
late dynamic response;
|
||||
try {
|
||||
response = await UserService.instance
|
||||
.getTokenForPasskeySession(widget.sessionID);
|
||||
} on PassKeySessionNotVerifiedError {
|
||||
showToast(context, context.l10n.passKeyPendingVerification);
|
||||
return;
|
||||
} on PassKeySessionExpiredError {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.loginSessionExpired,
|
||||
context.l10n.loginSessionExpiredDetails,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to check status", e, s);
|
||||
showGenericErrorDialog(context: context).ignore();
|
||||
return;
|
||||
}
|
||||
await UserService.instance.onPassKeyVerified(context, response);
|
||||
}
|
||||
|
||||
Future<void> _handleDeeplink(String? link) async {
|
||||
if (!context.mounted ||
|
||||
Configuration.instance.hasConfiguredAccount() ||
|
||||
@@ -66,8 +91,15 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
showToast(context, 'Account is already configured.');
|
||||
return;
|
||||
}
|
||||
final String? uri = Uri.parse(link).queryParameters['response'];
|
||||
String base64String = uri!.toString();
|
||||
final parsedUri = Uri.parse(link);
|
||||
final sessionID = parsedUri.queryParameters['passkeySessionID'];
|
||||
if (sessionID != widget.sessionID) {
|
||||
showToast(context, "Session ID mismatch");
|
||||
_logger.warning('ignored deeplink: sessionID mismatch');
|
||||
return;
|
||||
}
|
||||
final String? authResponse = parsedUri.queryParameters['response'];
|
||||
String base64String = authResponse!.toString();
|
||||
while (base64String.length % 4 != 0) {
|
||||
base64String += '=';
|
||||
}
|
||||
@@ -125,9 +157,23 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: context.l10n.verifyPasskey,
|
||||
labelText: context.l10n.tryAgain,
|
||||
onTap: () => launchPasskey(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.checkStatus,
|
||||
onTap: () async {
|
||||
try {
|
||||
await checkStatus();
|
||||
} catch (e) {
|
||||
debugPrint('failed to check status %e');
|
||||
showGenericErrorDialog(context: context).ignore();
|
||||
}
|
||||
},
|
||||
shouldSurfaceExecutionStates: true,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:typed_data';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/user_details.dart';
|
||||
import 'package:ente_auth/services/auth_feature_flag.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/services/passkey_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
@@ -66,20 +65,17 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
// We don't know if the user can disable MFA yet, so we fetch the info
|
||||
UserService.instance.getUserDetailsV2().ignore();
|
||||
}
|
||||
final bool isInternalUser =
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
children.addAll([
|
||||
if (isInternalUser) sectionOptionSpacing,
|
||||
if (isInternalUser)
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.passkey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async => await onPasskeyClick(context),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.passkey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async => await onPasskeyClick(context),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 3.0.9+309
|
||||
version: 3.0.10+310
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
@@ -81,3 +81,7 @@ class SrpSetupNotCompleteError extends Error {}
|
||||
class SharingNotPermittedForFreeAccountsError extends Error {}
|
||||
|
||||
class NoMediaLocationAccessError extends Error {}
|
||||
|
||||
class PassKeySessionNotVerifiedError extends Error {}
|
||||
|
||||
class PassKeySessionExpiredError extends Error {}
|
||||
|
||||
7
mobile/lib/generated/intl/messages_en.dart
generated
7
mobile/lib/generated/intl/messages_en.dart
generated
@@ -418,6 +418,7 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
MessageLookupByLibrary.simpleMessage("Check for updates"),
|
||||
"checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage(
|
||||
"Please check your inbox (and spam) to complete verification"),
|
||||
"checkStatus": MessageLookupByLibrary.simpleMessage("Check status"),
|
||||
"checking": MessageLookupByLibrary.simpleMessage("Checking..."),
|
||||
"claimFreeStorage":
|
||||
MessageLookupByLibrary.simpleMessage("Claim free storage"),
|
||||
@@ -908,6 +909,10 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"lockscreen": MessageLookupByLibrary.simpleMessage("Lockscreen"),
|
||||
"logInLabel": MessageLookupByLibrary.simpleMessage("Log in"),
|
||||
"loggingOut": MessageLookupByLibrary.simpleMessage("Logging out..."),
|
||||
"loginSessionExpired":
|
||||
MessageLookupByLibrary.simpleMessage("Session expired"),
|
||||
"loginSessionExpiredDetails": MessageLookupByLibrary.simpleMessage(
|
||||
"Your session has expired. Please login again."),
|
||||
"loginTerms": MessageLookupByLibrary.simpleMessage(
|
||||
"By clicking log in, I agree to the <u-terms>terms of service</u-terms> and <u-policy>privacy policy</u-policy>"),
|
||||
"logout": MessageLookupByLibrary.simpleMessage("Logout"),
|
||||
@@ -1020,6 +1025,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
||||
"pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"),
|
||||
"pairingComplete":
|
||||
MessageLookupByLibrary.simpleMessage("Pairing complete"),
|
||||
"passKeyPendingVerification": MessageLookupByLibrary.simpleMessage(
|
||||
"Verification is still pending"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
|
||||
"passkeyAuthTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Passkey verification"),
|
||||
|
||||
40
mobile/lib/generated/l10n.dart
generated
40
mobile/lib/generated/l10n.dart
generated
@@ -3162,6 +3162,16 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Check status`
|
||||
String get checkStatus {
|
||||
return Intl.message(
|
||||
'Check status',
|
||||
name: 'checkStatus',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Checking...`
|
||||
String get checking {
|
||||
return Intl.message(
|
||||
@@ -8408,6 +8418,36 @@ class S {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Verification is still pending`
|
||||
String get passKeyPendingVerification {
|
||||
return Intl.message(
|
||||
'Verification is still pending',
|
||||
name: 'passKeyPendingVerification',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Session expired`
|
||||
String get loginSessionExpired {
|
||||
return Intl.message(
|
||||
'Session expired',
|
||||
name: 'loginSessionExpired',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Your session has expired. Please login again.`
|
||||
String get loginSessionExpiredDetails {
|
||||
return Intl.message(
|
||||
'Your session has expired. Please login again.',
|
||||
name: 'loginSessionExpiredDetails',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Verify passkey`
|
||||
String get verifyPasskey {
|
||||
return Intl.message(
|
||||
|
||||
@@ -451,6 +451,7 @@
|
||||
"privacy": "Privacy",
|
||||
"terms": "Terms",
|
||||
"checkForUpdates": "Check for updates",
|
||||
"checkStatus": "Check status",
|
||||
"checking": "Checking...",
|
||||
"youAreOnTheLatestVersion": "You are on the latest version",
|
||||
"account": "Account",
|
||||
@@ -1198,6 +1199,9 @@
|
||||
"waitingForVerification": "Waiting for verification...",
|
||||
"passkey": "Passkey",
|
||||
"passkeyAuthTitle": "Passkey verification",
|
||||
"passKeyPendingVerification": "Verification is still pending",
|
||||
"loginSessionExpired" : "Session expired",
|
||||
"loginSessionExpiredDetails": "Your session has expired. Please login again.",
|
||||
"verifyPasskey": "Verify passkey",
|
||||
"playOnTv": "Play album on TV",
|
||||
"pair": "Pair",
|
||||
|
||||
@@ -16,6 +16,7 @@ import "package:photos/events/account_configured_event.dart";
|
||||
import 'package:photos/events/two_factor_status_change_event.dart';
|
||||
import 'package:photos/events/user_details_changed_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import "package:photos/models/api/user/srp.dart";
|
||||
import 'package:photos/models/delete_account.dart';
|
||||
@@ -308,23 +309,57 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPassKeyVerified(BuildContext context, Map response) async {
|
||||
final userPassword = Configuration.instance.getVolatilePassword();
|
||||
if (userPassword == null) throw Exception("volatile password is null");
|
||||
|
||||
await _saveConfiguration(response);
|
||||
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
Future<dynamic> getTokenForPasskeySession(String sessionID) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
"${_config.getHttpEndpoint()}/users/two-factor/passkeys/get-token",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
return response.data;
|
||||
} on DioError catch (e) {
|
||||
if (e.response != null) {
|
||||
if (e.response!.statusCode == 404 || e.response!.statusCode == 410) {
|
||||
throw PassKeySessionExpiredError();
|
||||
}
|
||||
if (e.response!.statusCode == 400) {
|
||||
throw PassKeySessionNotVerifiedError();
|
||||
}
|
||||
}
|
||||
rethrow;
|
||||
} catch (e, s) {
|
||||
_logger.severe("unexpected error", e, s);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
Future<void> onPassKeyVerified(BuildContext context, Map response) async {
|
||||
final ProgressDialog dialog =
|
||||
createProgressDialog(context, context.l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
final userPassword = Configuration.instance.getVolatilePassword();
|
||||
if (userPassword == null) throw Exception("volatile password is null");
|
||||
|
||||
await _saveConfiguration(response);
|
||||
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
await dialog.hide();
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyEmail(
|
||||
|
||||
@@ -3,13 +3,15 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/core/errors.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/account/two_factor.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import "package:photos/ui/components/buttons/button_widget.dart";
|
||||
import "package:photos/ui/components/models/button_type.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import "package:photos/utils/toast_util.dart";
|
||||
import "package:uni_links/uni_links.dart";
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyPage extends StatefulWidget {
|
||||
@@ -17,8 +19,8 @@ class PasskeyPage extends StatefulWidget {
|
||||
|
||||
const PasskeyPage(
|
||||
this.sessionID, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PasskeyPage> createState() => _PasskeyPageState();
|
||||
@@ -49,6 +51,30 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkStatus() async {
|
||||
late dynamic response;
|
||||
try {
|
||||
response = await UserService.instance
|
||||
.getTokenForPasskeySession(widget.sessionID);
|
||||
} on PassKeySessionNotVerifiedError {
|
||||
showToast(context, context.l10n.passKeyPendingVerification);
|
||||
return;
|
||||
} on PassKeySessionExpiredError {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.loginSessionExpired,
|
||||
context.l10n.loginSessionExpiredDetails,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to check status", e, s);
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
return;
|
||||
}
|
||||
await UserService.instance.onPassKeyVerified(context, response);
|
||||
}
|
||||
|
||||
Future<void> _handleDeeplink(String? link) async {
|
||||
if (!context.mounted ||
|
||||
Configuration.instance.hasConfiguredAccount() ||
|
||||
@@ -60,8 +86,20 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
}
|
||||
try {
|
||||
if (mounted && link.toLowerCase().startsWith("ente://passkey")) {
|
||||
final String? uri = Uri.parse(link).queryParameters['response'];
|
||||
String base64String = uri!.toString();
|
||||
if (Configuration.instance.isLoggedIn()) {
|
||||
_logger.info('ignored deeplink: already configured');
|
||||
showToast(context, 'Account is already configured.');
|
||||
return;
|
||||
}
|
||||
final parsedUri = Uri.parse(link);
|
||||
final sessionID = parsedUri.queryParameters['passkeySessionID'];
|
||||
if (sessionID != widget.sessionID) {
|
||||
showToast(context, "Session ID mismatch");
|
||||
_logger.warning('ignored deeplink: sessionID mismatch');
|
||||
return;
|
||||
}
|
||||
final String? authResponse = parsedUri.queryParameters['response'];
|
||||
String base64String = authResponse!.toString();
|
||||
while (base64String.length % 4 != 0) {
|
||||
base64String += '=';
|
||||
}
|
||||
@@ -90,10 +128,11 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
S.of(context).passkeyAuthTitle,
|
||||
l10n.passkeyAuthTitle,
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
@@ -108,7 +147,7 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).waitingForVerification,
|
||||
context.l10n.waitingForVerification,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
@@ -117,9 +156,23 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: S.of(context).verifyPasskey,
|
||||
labelText: context.l10n.tryAgain,
|
||||
onTap: () => launchPasskey(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.checkStatus,
|
||||
onTap: () async {
|
||||
try {
|
||||
await checkStatus();
|
||||
} catch (e) {
|
||||
debugPrint('failed to check status %e');
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
},
|
||||
shouldSurfaceExecutionStates: true,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -134,7 +187,7 @@ class _PasskeyPageState extends State<PasskeyPage> {
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).recoverAccount,
|
||||
context.l10n.recoverAccount,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:photos/events/two_factor_status_change_event.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import "package:photos/l10n/l10n.dart";
|
||||
import "package:photos/models/user_details.dart";
|
||||
import 'package:photos/service_locator.dart';
|
||||
import 'package:photos/services/local_authentication_service.dart';
|
||||
import "package:photos/services/passkey_service.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
@@ -101,17 +100,16 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (flagService.passKeyEnabled) sectionOptionSpacing,
|
||||
if (flagService.passKeyEnabled)
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.passkey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async => await onPasskeyClick(context),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.passkey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async => await onPasskeyClick(context),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
|
||||
@@ -12,7 +12,7 @@ description: ente photos application
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
|
||||
version: 0.8.137+657
|
||||
version: 0.8.138+658
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
||||
Reference in New Issue
Block a user