[auth][photos] Support for passkey (#435)
<!-- Thanks for contributing! Provide a description of your changes below and a general summary in the title Please look at the following checklist to ensure that your PR can be accepted quickly: --> ## Description Passkey implementation (similar will be done in ente Photos) <!--- Describe your changes in detail --> ## Type of Change <!--- Put an `x` in all the boxes that apply: --> - [ ] 🖼️ New icon - [x] ✨ New feature (non-breaking change which adds functionality) - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) - [ ] 🧹 Code refactor - [ ] ✅ Build configuration change - [ ] 📝 Documentation - [ ] 🗑️ Chore
This commit is contained in:
commit
4744434a62
23 changed files with 585 additions and 72 deletions
|
@ -56,11 +56,11 @@ android {
|
|||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : file(System.getenv("SIGNING_KEY_PATH"))
|
||||
keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
|
||||
storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
|
||||
}
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null
|
||||
keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
|
||||
storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "default"
|
||||
|
|
|
@ -35,6 +35,13 @@
|
|||
<data android:scheme="otpauth" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="enteauth" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>otpauth</string>
|
||||
<string>enteauth</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
|
|
@ -76,8 +76,8 @@ class EnteRequestInterceptor extends Interceptor {
|
|||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
if (kDebugMode) {
|
||||
assert(
|
||||
options.baseUrl == enteEndpoint,
|
||||
"interceptor should only be used for API endpoint",
|
||||
options.baseUrl == enteEndpoint,
|
||||
"interceptor should only be used for API endpoint",
|
||||
);
|
||||
}
|
||||
// ignore: prefer_const_constructors
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
},
|
||||
"contactSupport": "Contact support",
|
||||
"rateUsOnStore" : "Rate us on {storeName}",
|
||||
"rateUsOnStore": "Rate us on {storeName}",
|
||||
"blog": "Blog",
|
||||
"merchandise": "Merchandise",
|
||||
"verifyPassword": "Verify password",
|
||||
|
@ -133,7 +133,6 @@
|
|||
"faq_q_5": "How can I enable FaceID lock in ente Auth",
|
||||
"faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.",
|
||||
"somethingWentWrongMessage": "Something went wrong, please try again",
|
||||
|
||||
"leaveFamily": "Leave family",
|
||||
"leaveFamilyMessage": "Are you sure that you want to leave the family plan?",
|
||||
"inFamilyPlanMessage": "You are on a family plan!",
|
||||
|
@ -145,6 +144,7 @@
|
|||
"enterCodeHint": "Enter the 6-digit code from\nyour authenticator app",
|
||||
"lostDeviceTitle": "Lost device?",
|
||||
"twoFactorAuthTitle": "Two-factor authentication",
|
||||
"passkeyAuthTitle": "Passkey authentication",
|
||||
"recoverAccount": "Recover account",
|
||||
"enterRecoveryKeyHint": "Enter your recovery key",
|
||||
"recover": "Recover",
|
||||
|
@ -337,10 +337,10 @@
|
|||
"offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe.",
|
||||
"showLargeIcons": "Show large icons",
|
||||
"shouldHideCode": "Hide codes",
|
||||
"doubleTapToViewHiddenCode" : "You can double tap on an entry to view code",
|
||||
"doubleTapToViewHiddenCode": "You can double tap on an entry to view code",
|
||||
"focusOnSearchBar": "Focus search on app start",
|
||||
"confirmUpdatingkey": "Are you sure you want to update the secret key?",
|
||||
"minimizeAppOnCopy": "Minimize app on copy",
|
||||
"minimizeAppOnCopy": "Minimize app on copy",
|
||||
"editCodeAuthMessage": "Authenticate to edit code",
|
||||
"deleteCodeAuthMessage": "Authenticate to delete code",
|
||||
"showQRAuthMessage": "Authenticate to show QR code",
|
||||
|
@ -405,5 +405,8 @@
|
|||
"signOutOtherDevices": "Sign out other devices",
|
||||
"doNotSignOut": "Do not sign out",
|
||||
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
|
||||
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!"
|
||||
}
|
||||
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
|
||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
||||
"passkey": "Passkey"
|
||||
}
|
23
auth/lib/services/auth_feature_flag.dart
Normal file
23
auth/lib/services/auth_feature_flag.dart
Normal file
|
@ -0,0 +1,23 @@
|
|||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FeatureFlagService {
|
||||
FeatureFlagService._privateConstructor();
|
||||
static final FeatureFlagService instance =
|
||||
FeatureFlagService._privateConstructor();
|
||||
|
||||
static final _internalUserIDs = const String.fromEnvironment(
|
||||
"internal_user_ids",
|
||||
defaultValue: "1,2,3,4,191,125,1580559962388044,1580559962392434,10000025",
|
||||
).split(",").map((element) {
|
||||
return int.parse(element);
|
||||
}).toSet();
|
||||
|
||||
bool isInternalUserOrDebugBuild() {
|
||||
final String? email = Configuration.instance.getEmail();
|
||||
final userID = Configuration.instance.getUserID();
|
||||
return (email != null && email.endsWith("@ente.io")) ||
|
||||
_internalUserIDs.contains(userID) ||
|
||||
kDebugMode;
|
||||
}
|
||||
}
|
33
auth/lib/services/passkey_service.dart
Normal file
33
auth/lib/services/passkey_service.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyService {
|
||||
PasskeyService._privateConstructor();
|
||||
static final PasskeyService instance = PasskeyService._privateConstructor();
|
||||
|
||||
final _enteDio = Network.instance.enteDio;
|
||||
|
||||
Future<String> getJwtToken() async {
|
||||
final response = await _enteDio.get(
|
||||
"/users/accounts-token",
|
||||
);
|
||||
return response.data!["accountsToken"] as String;
|
||||
}
|
||||
|
||||
Future<void> openPasskeyPage(BuildContext context) async {
|
||||
try {
|
||||
final jwtToken = await getJwtToken();
|
||||
final url = "https://accounts.ente.io/account-handoff?token=$jwtToken";
|
||||
await launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
Logger('PasskeyService').severe("failed to open passkey page", e);
|
||||
showGenericErrorDialog(context: context).ignore();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
|||
import 'package:ente_auth/ui/account/recovery_page.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/passkey_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
|
@ -264,6 +265,33 @@ 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);
|
||||
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
|
||||
userPassword,
|
||||
Configuration.instance.getKeyAttributes()!,
|
||||
);
|
||||
page = const HomePage();
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> verifyEmail(
|
||||
BuildContext context,
|
||||
String ott, {
|
||||
|
@ -487,9 +515,9 @@ class UserService {
|
|||
final clientS = client.calculateSecret(serverB);
|
||||
final clientM = client.calculateClientEvidenceMessage();
|
||||
// ignore: unused_local_variable
|
||||
late Response srpCompleteResponse;
|
||||
late Response _;
|
||||
if (setKeysRequest == null) {
|
||||
srpCompleteResponse = await _enteDio.post(
|
||||
_ = await _enteDio.post(
|
||||
"/users/srp/complete",
|
||||
data: {
|
||||
'setupID': setupSRPResponse.setupID,
|
||||
|
@ -497,7 +525,7 @@ class UserService {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
srpCompleteResponse = await _enteDio.post(
|
||||
_ = await _enteDio.post(
|
||||
"/users/srp/update",
|
||||
data: {
|
||||
'setupID': setupSRPResponse.setupID,
|
||||
|
@ -581,11 +609,15 @@ class UserService {
|
|||
},
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
Widget? page;
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
Configuration.instance.setVolatilePassword(userPassword);
|
||||
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(passkeySessionID);
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
|
@ -603,7 +635,7 @@ class UserService {
|
|||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return page;
|
||||
return page!;
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
|
@ -861,16 +893,19 @@ class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _saveConfiguration(Response response) async {
|
||||
await Configuration.instance.setUserID(response.data["id"]);
|
||||
if (response.data["encryptedToken"] != null) {
|
||||
Future<void> _saveConfiguration(dynamic response) async {
|
||||
final responseData = response is Map ? response : response.data as Map?;
|
||||
if (responseData == null) return;
|
||||
|
||||
await Configuration.instance.setUserID(responseData["id"]);
|
||||
if (responseData["encryptedToken"] != null) {
|
||||
await Configuration.instance
|
||||
.setEncryptedToken(response.data["encryptedToken"]);
|
||||
.setEncryptedToken(responseData["encryptedToken"]);
|
||||
await Configuration.instance.setKeyAttributes(
|
||||
KeyAttributes.fromMap(response.data["keyAttributes"]),
|
||||
KeyAttributes.fromMap(responseData["keyAttributes"]),
|
||||
);
|
||||
} else {
|
||||
await Configuration.instance.setToken(response.data["token"]);
|
||||
await Configuration.instance.setToken(responseData["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ class OfflineAuthenticatorDB {
|
|||
static const entityTable = 'entities';
|
||||
|
||||
OfflineAuthenticatorDB._privateConstructor();
|
||||
static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor();
|
||||
static final OfflineAuthenticatorDB instance =
|
||||
OfflineAuthenticatorDB._privateConstructor();
|
||||
|
||||
static Future<Database>? _dbFuture;
|
||||
|
||||
|
@ -26,7 +27,7 @@ class OfflineAuthenticatorDB {
|
|||
|
||||
Future<Database> _initDatabase() async {
|
||||
final Directory documentsDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
await getApplicationDocumentsDirectory();
|
||||
final String path = join(documentsDirectory.path, _databaseName);
|
||||
debugPrint(path);
|
||||
return await openDatabase(
|
||||
|
@ -70,10 +71,10 @@ class OfflineAuthenticatorDB {
|
|||
}
|
||||
|
||||
Future<int> updateEntry(
|
||||
int generatedID,
|
||||
String encData,
|
||||
String header,
|
||||
) async {
|
||||
int generatedID,
|
||||
String encData,
|
||||
String header,
|
||||
) async {
|
||||
final db = await instance.database;
|
||||
final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch;
|
||||
int affectedRows = await db.update(
|
||||
|
|
115
auth/lib/ui/passkey_page.dart
Normal file
115
auth/lib/ui/passkey_page.dart
Normal file
|
@ -0,0 +1,115 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
||||
const PasskeyPage(
|
||||
this.sessionID, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PasskeyPage> createState() => _PasskeyPageState();
|
||||
}
|
||||
|
||||
class _PasskeyPageState extends State<PasskeyPage> {
|
||||
final Logger _logger = Logger("PasskeyPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
launchPasskey();
|
||||
_initDeepLinks();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> launchPasskey() async {
|
||||
await launchUrlString(
|
||||
"https://accounts.ente.io/passkeys/flow?"
|
||||
"passkeySessionID=${widget.sessionID}"
|
||||
"&redirect=enteauth://passkey",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleDeeplink(String? link) async {
|
||||
if (!context.mounted ||
|
||||
Configuration.instance.hasConfiguredAccount() ||
|
||||
link == null) {
|
||||
return;
|
||||
}
|
||||
if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
|
||||
final uri = Uri.parse(link).queryParameters['response'];
|
||||
// response to json
|
||||
final res = utf8.decode(base64.decode(uri!));
|
||||
final json = jsonDecode(res) as Map<String, dynamic>;
|
||||
await UserService.instance.onPassKeyVerified(context, json);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _initDeepLinks() async {
|
||||
// Attach a listener to the stream
|
||||
linkStream.listen(
|
||||
_handleDeeplink,
|
||||
onError: (err) {
|
||||
_logger.severe(err);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
l10n.passkeyAuthTitle,
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
l10n.waitingForBrowserRequest,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ElevatedButton(
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: launchPasskey,
|
||||
child: Text(l10n.launchPasskeyUrlAgain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
|
|
|
@ -4,7 +4,9 @@ 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';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/request_pwd_verification_page.dart';
|
||||
|
@ -61,7 +63,21 @@ 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: () => PasskeyService.instance.openPasskeyPage(context),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: l10n.emailVerificationToggle,
|
||||
|
|
|
@ -197,10 +197,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
version: "1.18.0"
|
||||
computer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -262,10 +262,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.3"
|
||||
version: "1.7.2"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -751,6 +751,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -811,26 +835,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.11.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -931,10 +955,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1304,10 +1328,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.11.1"
|
||||
step_progress_indicator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1320,10 +1344,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1368,26 +1392,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
|
||||
sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.24.3"
|
||||
version: "1.24.9"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.6.1"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
|
||||
sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.3"
|
||||
version: "0.5.9"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1560,10 +1584,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.0"
|
||||
version: "13.0.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1572,14 +1596,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1637,5 +1653,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||
dart: ">=3.2.0-0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
|
|
@ -8,7 +8,8 @@ const String sentryDSN =
|
|||
const String sentryDebugDSN =
|
||||
"https://ca5e686dd7f149d9bf94e620564cceba@sentry.ente.io/3";
|
||||
const String sentryTunnel = "https://sentry-reporter.ente.io";
|
||||
const String githubDiscussionsUrl = "https://github.com/ente-io/ente/discussions";
|
||||
const String githubDiscussionsUrl =
|
||||
"https://github.com/ente-io/ente/discussions";
|
||||
const int microSecondsInDay = 86400000000;
|
||||
const int android11SDKINT = 30;
|
||||
const int jan011981Time = 347155200000000;
|
||||
|
@ -41,6 +42,7 @@ const supportEmail = 'support@ente.io';
|
|||
class FFDefault {
|
||||
static const bool enableStripe = true;
|
||||
static const bool disableCFWorker = false;
|
||||
static const bool enablePasskey = false;
|
||||
}
|
||||
|
||||
const kDefaultProductionEndpoint = 'https://api.ente.io';
|
||||
|
|
7
mobile/lib/generated/intl/messages_en.dart
generated
7
mobile/lib/generated/intl/messages_en.dart
generated
|
@ -797,6 +797,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"Kindly help us with this information"),
|
||||
"language": MessageLookupByLibrary.simpleMessage("Language"),
|
||||
"lastUpdated": MessageLookupByLibrary.simpleMessage("Last updated"),
|
||||
"launchPasskeyUrlAgain":
|
||||
MessageLookupByLibrary.simpleMessage("Launch passkey URL again"),
|
||||
"leave": MessageLookupByLibrary.simpleMessage("Leave"),
|
||||
"leaveAlbum": MessageLookupByLibrary.simpleMessage("Leave album"),
|
||||
"leaveFamily": MessageLookupByLibrary.simpleMessage("Leave family"),
|
||||
|
@ -954,6 +956,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"orPickAnExistingOne":
|
||||
MessageLookupByLibrary.simpleMessage("Or pick an existing one"),
|
||||
"pair": MessageLookupByLibrary.simpleMessage("Pair"),
|
||||
"passkey": MessageLookupByLibrary.simpleMessage("Passkey"),
|
||||
"passkeyAuthTitle":
|
||||
MessageLookupByLibrary.simpleMessage("Passkey authentication"),
|
||||
"password": MessageLookupByLibrary.simpleMessage("Password"),
|
||||
"passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage(
|
||||
"Password changed successfully"),
|
||||
|
@ -1460,6 +1465,8 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||
"viewer": MessageLookupByLibrary.simpleMessage("Viewer"),
|
||||
"visitWebToManage": MessageLookupByLibrary.simpleMessage(
|
||||
"Please visit web.ente.io to manage your subscription"),
|
||||
"waitingForBrowserRequest": MessageLookupByLibrary.simpleMessage(
|
||||
"Waiting for browser request..."),
|
||||
"waitingForWifi":
|
||||
MessageLookupByLibrary.simpleMessage("Waiting for WiFi..."),
|
||||
"weAreOpenSource":
|
||||
|
|
40
mobile/lib/generated/l10n.dart
generated
40
mobile/lib/generated/l10n.dart
generated
|
@ -8308,6 +8308,46 @@ class S {
|
|||
);
|
||||
}
|
||||
|
||||
/// `Waiting for browser request...`
|
||||
String get waitingForBrowserRequest {
|
||||
return Intl.message(
|
||||
'Waiting for browser request...',
|
||||
name: 'waitingForBrowserRequest',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Launch passkey URL again`
|
||||
String get launchPasskeyUrlAgain {
|
||||
return Intl.message(
|
||||
'Launch passkey URL again',
|
||||
name: 'launchPasskeyUrlAgain',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Passkey`
|
||||
String get passkey {
|
||||
return Intl.message(
|
||||
'Passkey',
|
||||
name: 'passkey',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Passkey authentication`
|
||||
String get passkeyAuthTitle {
|
||||
return Intl.message(
|
||||
'Passkey authentication',
|
||||
name: 'passkeyAuthTitle',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Play album on TV`
|
||||
String get playOnTv {
|
||||
return Intl.message(
|
||||
|
|
|
@ -1188,6 +1188,10 @@
|
|||
"changeLocationOfSelectedItems": "Change location of selected items?",
|
||||
"editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente",
|
||||
"cleanUncategorized": "Clean Uncategorized",
|
||||
"waitingForBrowserRequest": "Waiting for browser request...",
|
||||
"launchPasskeyUrlAgain": "Launch passkey URL again",
|
||||
"passkey": "Passkey",
|
||||
"passkeyAuthTitle": "Passkey authentication",
|
||||
"playOnTv": "Play album on TV",
|
||||
"pair": "Pair",
|
||||
"deviceNotFound": "Device not found",
|
||||
|
|
|
@ -68,6 +68,18 @@ class FeatureFlagService {
|
|||
}
|
||||
}
|
||||
|
||||
bool enablePasskey() {
|
||||
try {
|
||||
if (isInternalUserOrDebugBuild()) {
|
||||
return true;
|
||||
}
|
||||
return _getFeatureFlags().enablePasskey;
|
||||
} catch (e) {
|
||||
_logger.info('error in enablePasskey check', e);
|
||||
return FFDefault.enablePasskey;
|
||||
}
|
||||
}
|
||||
|
||||
bool isInternalUserOrDebugBuild() {
|
||||
final String? email = Configuration.instance.getEmail();
|
||||
final userID = Configuration.instance.getUserID();
|
||||
|
@ -94,20 +106,24 @@ class FeatureFlags {
|
|||
static FeatureFlags defaultFlags = FeatureFlags(
|
||||
disableCFWorker: FFDefault.disableCFWorker,
|
||||
enableStripe: FFDefault.enableStripe,
|
||||
enablePasskey: FFDefault.enablePasskey,
|
||||
);
|
||||
|
||||
final bool disableCFWorker;
|
||||
final bool enableStripe;
|
||||
final bool enablePasskey;
|
||||
|
||||
FeatureFlags({
|
||||
required this.disableCFWorker,
|
||||
required this.enableStripe,
|
||||
required this.enablePasskey,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
"disableCFWorker": disableCFWorker,
|
||||
"enableStripe": enableStripe,
|
||||
"enablePasskey": enablePasskey,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -120,6 +136,7 @@ class FeatureFlags {
|
|||
return FeatureFlags(
|
||||
disableCFWorker: json["disableCFWorker"] ?? FFDefault.disableCFWorker,
|
||||
enableStripe: json["enableStripe"] ?? FFDefault.enableStripe,
|
||||
enablePasskey: json["enablePasskey"] ?? FFDefault.enablePasskey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
33
mobile/lib/services/passkey_service.dart
Normal file
33
mobile/lib/services/passkey_service.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import "package:flutter/cupertino.dart";
|
||||
import "package:logging/logging.dart";
|
||||
import "package:photos/core/network/network.dart";
|
||||
import "package:photos/utils/dialog_util.dart";
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyService {
|
||||
PasskeyService._privateConstructor();
|
||||
static final PasskeyService instance = PasskeyService._privateConstructor();
|
||||
|
||||
final _enteDio = NetworkClient.instance.enteDio;
|
||||
|
||||
Future<String> getJwtToken() async {
|
||||
final response = await _enteDio.get(
|
||||
"/users/accounts-token",
|
||||
);
|
||||
return response.data!["accountsToken"] as String;
|
||||
}
|
||||
|
||||
Future<void> openPasskeyPage(BuildContext context) async {
|
||||
try {
|
||||
final jwtToken = await getJwtToken();
|
||||
final url = "https://accounts.ente.io/account-handoff?token=$jwtToken";
|
||||
await launchUrlString(
|
||||
url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
Logger('PasskeyService').severe("failed to open passkey page", e);
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ import 'package:photos/models/set_recovery_key_request.dart';
|
|||
import 'package:photos/models/user_details.dart';
|
||||
import 'package:photos/ui/account/login_page.dart';
|
||||
import 'package:photos/ui/account/ott_verification_page.dart';
|
||||
import "package:photos/ui/account/passkey_page.dart";
|
||||
import 'package:photos/ui/account/password_entry_page.dart';
|
||||
import 'package:photos/ui/account/password_reentry_page.dart';
|
||||
import "package:photos/ui/account/recovery_page.dart";
|
||||
|
@ -314,6 +315,25 @@ 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()!,
|
||||
);
|
||||
} else {
|
||||
throw Exception("unexpected response during passkey verification");
|
||||
}
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
Bus.instance.fire(AccountConfiguredEvent());
|
||||
}
|
||||
|
||||
Future<void> verifyEmail(
|
||||
BuildContext context,
|
||||
String ott, {
|
||||
|
@ -648,10 +668,14 @@ class UserService {
|
|||
if (response.statusCode == 200) {
|
||||
Widget page;
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
final String passkeySessionID = response.data["passkeySessionID"];
|
||||
|
||||
Configuration.instance.setVolatilePassword(userPassword);
|
||||
if (twoFASessionID.isNotEmpty) {
|
||||
await setTwoFactor(value: true);
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(passkeySessionID);
|
||||
} else {
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
|
@ -1108,16 +1132,19 @@ class UserService {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _saveConfiguration(Response response) async {
|
||||
await Configuration.instance.setUserID(response.data["id"]);
|
||||
if (response.data["encryptedToken"] != null) {
|
||||
Future<void> _saveConfiguration(dynamic response) async {
|
||||
final responseData = response is Map ? response : response.data as Map?;
|
||||
if (responseData == null) return;
|
||||
|
||||
await Configuration.instance.setUserID(responseData["id"]);
|
||||
if (responseData["encryptedToken"] != null) {
|
||||
await Configuration.instance
|
||||
.setEncryptedToken(response.data["encryptedToken"]);
|
||||
.setEncryptedToken(responseData["encryptedToken"]);
|
||||
await Configuration.instance.setKeyAttributes(
|
||||
KeyAttributes.fromMap(response.data["keyAttributes"]),
|
||||
KeyAttributes.fromMap(responseData["keyAttributes"]),
|
||||
);
|
||||
} else {
|
||||
await Configuration.instance.setToken(response.data["token"]);
|
||||
await Configuration.instance.setToken(responseData["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
118
mobile/lib/ui/account/passkey_page.dart
Normal file
118
mobile/lib/ui/account/passkey_page.dart
Normal file
|
@ -0,0 +1,118 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photos/core/configuration.dart';
|
||||
import 'package:photos/ente_theme_data.dart';
|
||||
import "package:photos/generated/l10n.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:uni_links/uni_links.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
||||
const PasskeyPage(
|
||||
this.sessionID, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<PasskeyPage> createState() => _PasskeyPageState();
|
||||
}
|
||||
|
||||
class _PasskeyPageState extends State<PasskeyPage> {
|
||||
final Logger _logger = Logger("PasskeyPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
launchPasskey();
|
||||
_initDeepLinks();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> launchPasskey() async {
|
||||
await launchUrlString(
|
||||
"https://accounts.ente.io/passkeys/flow?"
|
||||
"passkeySessionID=${widget.sessionID}"
|
||||
"&redirect=ente://passkey",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleDeeplink(String? link) async {
|
||||
if (!context.mounted ||
|
||||
Configuration.instance.hasConfiguredAccount() ||
|
||||
link == null) {
|
||||
return;
|
||||
}
|
||||
if (mounted && link.toLowerCase().startsWith("ente://passkey")) {
|
||||
final uri = Uri.parse(link).queryParameters['response'];
|
||||
|
||||
// response to json
|
||||
final res = utf8.decode(base64.decode(uri!));
|
||||
final json = jsonDecode(res) as Map<String, dynamic>;
|
||||
|
||||
try {
|
||||
await UserService.instance.onPassKeyVerified(context, json);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _initDeepLinks() async {
|
||||
// Attach a listener to the stream
|
||||
linkStream.listen(
|
||||
_handleDeeplink,
|
||||
onError: (err) {
|
||||
_logger.severe(err);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
S.of(context).passkeyAuthTitle,
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).waitingForBrowserRequest,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ElevatedButton(
|
||||
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
|
||||
onPressed: launchPasskey,
|
||||
child: Text(S.of(context).launchPasskeyUrlAgain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,8 +7,11 @@ import 'package:photos/core/event_bus.dart';
|
|||
import 'package:photos/ente_theme_data.dart';
|
||||
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/services/feature_flag_service.dart";
|
||||
import 'package:photos/services/local_authentication_service.dart';
|
||||
import "package:photos/services/passkey_service.dart";
|
||||
import 'package:photos/services/user_service.dart';
|
||||
import 'package:photos/theme/ente_theme.dart';
|
||||
import "package:photos/ui/account/request_pwd_verification_page.dart";
|
||||
|
@ -65,6 +68,8 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
final Completer completer = Completer();
|
||||
final List<Widget> children = [];
|
||||
if (_config.hasConfiguredAccount()) {
|
||||
final bool isInternalUser =
|
||||
FeatureFlagService.instance.isInternalUserOrDebugBuild();
|
||||
children.addAll(
|
||||
[
|
||||
sectionOptionSpacing,
|
||||
|
@ -96,6 +101,17 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (isInternalUser) sectionOptionSpacing,
|
||||
if (isInternalUser)
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
title: context.l10n.passkey,
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () => PasskeyService.instance.openPasskeyPage(context),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: CaptionedTextWidget(
|
||||
|
|
|
@ -181,7 +181,7 @@ dependency_overrides:
|
|||
# current fork of tfite_flutter_helper depends on ffi: ^1.x.x
|
||||
# but we need ffi: ^2.0.1 for newer packages. The original tfite_flutter_helper
|
||||
#
|
||||
ffi: ^2.0.0
|
||||
ffi: ^2.1.0
|
||||
video_player:
|
||||
git:
|
||||
url: https://github.com/ente-io/packages.git
|
||||
|
|
Loading…
Add table
Reference in a new issue