Merge branch 'master' into redesign-with-components

This commit is contained in:
ashilkn 2022-12-06 15:27:27 +05:30
commit 6b5ef752b5
10 changed files with 461 additions and 43 deletions

View file

@ -323,6 +323,32 @@ class Configuration {
);
}
Future<void> verifyPassword(String password) async {
final KeyAttributes attributes = getKeyAttributes()!;
_logger.info('state validation done');
final kek = await CryptoUtil.deriveKey(
utf8.encode(password) as Uint8List,
Sodium.base642bin(attributes.kekSalt),
attributes.memLimit,
attributes.opsLimit,
).onError((e, s) {
_logger.severe('deriveKey failed', e, s);
throw KeyDerivationError();
});
_logger.info('user-key done');
try {
final Uint8List key = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedKey),
kek,
Sodium.base642bin(attributes.keyDecryptionNonce),
);
} catch (e) {
_logger.severe('master-key failed, incorrect password?', e);
throw Exception("Incorrect password");
}
}
Future<KeyAttributes> createNewRecoveryKey() async {
final masterKey = getKey()!;
final existingAttributes = getKeyAttributes();

View file

@ -7,7 +7,6 @@ import 'package:dio/dio.dart';
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/network.dart';
import 'package:photos/models/billing_plan.dart';
@ -30,9 +29,7 @@ class BillingService {
static final BillingService instance = BillingService._privateConstructor();
final _logger = Logger("BillingService");
final _dio = Network.instance.getDio();
final _enteDio = Network.instance.enteDio;
final _config = Configuration.instance;
bool _isOnSubscriptionPage = false;
@ -69,23 +66,16 @@ class BillingService {
}
Future<BillingPlans> getBillingPlans() {
_future ??= (_config.isLoggedIn()
? _fetchPublicBillingPlans()
: _fetchPrivateBillingPlans())
.then((response) {
_future ??= _fetchBillingPlans().then((response) {
return BillingPlans.fromMap(response.data);
});
return _future;
}
Future<Response<dynamic>> _fetchPrivateBillingPlans() {
Future<Response<dynamic>> _fetchBillingPlans() {
return _enteDio.get("/billing/user-plans/");
}
Future<Response<dynamic>> _fetchPublicBillingPlans() {
return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
}
Future<Subscription> verifySubscription(
final productID,
final verificationData, {

View file

@ -38,7 +38,8 @@ class UpdateService {
return _prefs.setInt(changeLogVersionKey, currentChangeLogVersion);
}
Future<bool> resetChangeLog() {
Future<bool> resetChangeLog() async {
await _prefs.remove("userNotify.passwordReminderFlag");
return _prefs.remove(changeLogVersionKey);
}

View file

@ -20,6 +20,8 @@ class UserRemoteFlagService {
UserRemoteFlagService._privateConstructor();
static const String recoveryVerificationFlag = "recoveryKeyVerified";
static const String _passwordReminderFlag = "userNotify"
".passwordReminderFlag";
static const String needRecoveryKeyVerification =
"needRecoveryKeyVerification";
@ -27,6 +29,20 @@ class UserRemoteFlagService {
_prefs = await SharedPreferences.getInstance();
}
bool showPasswordReminder() {
if (Platform.isAndroid) {
return false;
}
return !_prefs.containsKey(_passwordReminderFlag);
}
Future<bool> stopPasswordReminder() async {
if (Platform.isAndroid) {
return Future.value(true);
}
return _prefs.setBool(_passwordReminderFlag, true);
}
bool shouldShowRecoveryVerification() {
if (!_prefs.containsKey(needRecoveryKeyVerification)) {
// fetch the status from remote
@ -46,14 +62,13 @@ class UserRemoteFlagService {
// recovery key in the past or not. This helps in avoid showing the same
// prompt to the user on re-install or signing into a different device
Future<void> markRecoveryVerificationAsDone() async {
await _updateKeyValue(recoveryVerificationFlag, true.toString());
await _updateKeyValue(_passwordReminderFlag, true.toString());
await _prefs.setBool(needRecoveryKeyVerification, false);
}
Future<void> _refreshRecoveryVerificationFlag() async {
_logger.finest('refresh recovery key verification flag');
final remoteStatusValue =
await _getValue(recoveryVerificationFlag, "false");
final remoteStatusValue = await _getValue(_passwordReminderFlag, "false");
final bool isNeedVerificationFlagSet =
_prefs.containsKey(needRecoveryKeyVerification);
if (remoteStatusValue.toLowerCase() == "true") {

View file

@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/services/user_remote_flag_service.dart';
import 'package:photos/ui/account/email_entry_page.dart';
import 'package:photos/ui/account/login_page.dart';
import 'package:photos/ui/account/password_entry_page.dart';
@ -154,6 +155,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
void _navigateToSignUpPage() {
UpdateService.instance.hideChangeLog().ignore();
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
Widget page;
if (Configuration.instance.getEncryptedToken() == null) {
page = const EmailEntryPage();
@ -181,6 +183,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
void _navigateToSignInPage() {
UpdateService.instance.hideChangeLog().ignore();
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
Widget page;
if (Configuration.instance.getEncryptedToken() == null) {
page = const LoginPage();

View file

@ -25,6 +25,7 @@ import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/local_sync_service.dart';
import 'package:photos/services/update_service.dart';
import 'package:photos/services/user_remote_flag_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/states/user_details_state.dart';
import 'package:photos/theme/colors.dart';
@ -41,6 +42,7 @@ import 'package:photos/ui/home/landing_page_widget.dart';
import 'package:photos/ui/home/preserve_footer_widget.dart';
import 'package:photos/ui/home/start_backup_hook_widget.dart';
import 'package:photos/ui/loading_photos_widget.dart';
import 'package:photos/ui/notification/prompts/password_reminder.dart';
import 'package:photos/ui/notification/update/change_log_page.dart';
import 'package:photos/ui/settings/app_update_dialog.dart';
import 'package:photos/ui/settings_page.dart';
@ -318,6 +320,10 @@ class _HomeWidgetState extends State<HomeWidget> {
if (!LocalSyncService.instance.hasCompletedFirstImport()) {
return const LoadingPhotosWidget();
}
if (UserRemoteFlagService.instance.showPasswordReminder()) {
return const PasswordReminder();
}
if (_sharedFiles != null && _sharedFiles.isNotEmpty) {
ReceiveSharingIntent.reset();
return CreateCollectionPage(null, _sharedFiles);

View file

@ -0,0 +1,371 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:pedantic/pedantic.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/services/local_authentication_service.dart';
import 'package:photos/services/user_remote_flag_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/account/password_entry_page.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/home_widget.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
class PasswordReminder extends StatefulWidget {
const PasswordReminder({Key? key}) : super(key: key);
@override
State<PasswordReminder> createState() => _PasswordReminderState();
}
class _PasswordReminderState extends State<PasswordReminder> {
final _passwordController = TextEditingController();
final Logger _logger = Logger((_PasswordReminderState).toString());
bool _password2Visible = false;
bool _incorrectPassword = false;
Future<void> _verifyRecoveryKey() async {
final dialog = createProgressDialog(context, "Verifying password...");
await dialog.show();
try {
final String inputKey = _passwordController.text;
await Configuration.instance.verifyPassword(inputKey);
await dialog.hide();
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
// todo: change this as per figma once the component is ready
await showErrorDialog(
context,
"Password verified",
"Great! Thank you for verifying.\n"
"\nPlease"
" remember to keep your recovery key safely backed up.",
);
unawaited(
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomeWidget();
},
),
(route) => false,
),
);
} catch (e, s) {
_logger.severe("failed to verify password", e, s);
await dialog.hide();
_incorrectPassword = true;
if (mounted) {
setState(() => {});
}
}
}
Future<void> _onChangePasswordClick() async {
try {
final hasAuthenticated =
await LocalAuthenticationService.instance.requestLocalAuthentication(
context,
"Please authenticate to change your password",
);
if (hasAuthenticated) {
UserRemoteFlagService.instance.stopPasswordReminder().ignore();
await routeToPage(
context,
const PasswordEntryPage(
mode: PasswordEntryMode.update,
),
forceCustomPageRoute: true,
);
unawaited(
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomeWidget();
},
),
(route) => false,
),
);
}
} catch (e) {
showGenericErrorDialog(context);
return;
}
}
Future<void> _onSkipClick() async {
final enteTextTheme = getEnteTextTheme(context);
final enteColor = getEnteColorScheme(context);
final content = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"You will not be able to access your photos if you forget "
"your password.\n\nIf you do not remember your password, "
"now is a good time to change it.",
style: enteTextTheme.body.copyWith(
color: enteColor.textMuted,
),
),
const Padding(padding: EdgeInsets.all(8)),
SizedBox(
width: double.infinity,
height: 52,
child: OutlinedButton(
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
(Set<MaterialState> states) {
return enteTextTheme.bodyBold;
},
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
_onChangePasswordClick();
},
child: const Text(
"Change password",
),
),
),
const Padding(padding: EdgeInsets.all(8)),
SizedBox(
width: double.infinity,
height: 52,
child: OutlinedButton(
style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
textStyle: MaterialStateProperty.resolveWith<TextStyle>(
(Set<MaterialState> states) {
return enteTextTheme.bodyBold;
},
),
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return enteColor.fillFaint;
},
),
foregroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
return Theme.of(context).colorScheme.defaultTextColor;
},
),
),
onPressed: () async {
Navigator.of(context, rootNavigator: true).pop('dialog');
},
child: Text(
"Cancel",
style: enteTextTheme.bodyBold,
),
),
)
],
);
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: enteColor.backgroundElevated,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.report_outlined,
size: 36,
color: getEnteColorScheme(context).strokeBase,
),
],
),
content: content,
);
},
barrierColor: enteColor.backdropBaseMute,
);
}
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final enteTheme = Theme.of(context).colorScheme.enteTheme;
final List<Widget> actions = <Widget>[];
actions.add(
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
value: 1,
child: SizedBox(
width: 120,
height: 32,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.report_outlined,
color: warning500,
size: 20,
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
Text(
"Skip",
style: getEnteTextTheme(context)
.bodyBold
.copyWith(color: warning500),
),
],
),
),
),
];
},
onSelected: (value) async {
_onSkipClick();
},
),
);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: null,
automaticallyImplyLeading: false,
actions: actions,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'Password reminder',
style: enteTheme.textTheme.h3Bold,
),
Text(
Configuration.instance.getEmail()!,
style: enteTheme.textTheme.small.copyWith(
color: enteTheme.colorScheme.textMuted,
),
),
],
),
),
const SizedBox(height: 18),
Text(
"Enter your password to ensure you remember it."
"\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
style: enteTheme.textTheme.small
.copyWith(color: enteTheme.colorScheme.textMuted),
),
const SizedBox(height: 24),
TextFormField(
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
filled: true,
hintText: "Password",
suffixIcon: IconButton(
icon: Icon(
_password2Visible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
setState(() {
_password2Visible = !_password2Visible;
});
},
),
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
),
style: const TextStyle(
fontSize: 14,
fontFeatures: [FontFeature.tabularFigures()],
),
controller: _passwordController,
autofocus: false,
autocorrect: false,
obscureText: !_password2Visible,
keyboardType: TextInputType.visiblePassword,
onChanged: (_) {
_incorrectPassword = false;
setState(() {});
},
),
_incorrectPassword
? const SizedBox(height: 2)
: const SizedBox.shrink(),
_incorrectPassword
? Align(
alignment: Alignment.centerLeft,
child: Text(
"Incorrect password",
style: enteTheme.textTheme.small.copyWith(
color: enteTheme.colorScheme.warning700,
),
),
)
: const SizedBox.shrink(),
const SizedBox(height: 12),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GradientButton(
onTap: _verifyRecoveryKey,
text: "Verify",
),
const SizedBox(height: 8),
],
),
),
),
const SizedBox(height: 20)
],
),
),
),
);
},
),
),
);
}
}

View file

@ -45,7 +45,7 @@ Map<int, String> _days = {
7: "Sun",
};
final currentYear = int.parse(DateTime.now().year.toString());
final currentYear = DateTime.now().year;
const searchStartYear = 1970;
//Jun 2022
@ -267,30 +267,18 @@ bool isValidDate({
return true;
}
@Deprecated("Use parseDateTimeV2 ")
DateTime? parseDateFromFileName(String fileName) {
if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
// Whatsapp media files
return DateTime.tryParse(fileName.split('-')[1]);
} else if (fileName.startsWith("Screenshot_")) {
// Screenshots on droid
return DateTime.tryParse(
(fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'),
);
} else {
return DateTime.tryParse(
(fileName)
.replaceAll("IMG_", "")
.replaceAll("VID_", "")
.replaceAll("DCIM_", "")
.replaceAll("_", " "),
);
}
}
final RegExp exp = RegExp('[\\.A-Za-z]*');
DateTime? parseDateTimeFromFileNameV2(String fileName) {
DateTime? parseDateTimeFromFileNameV2(
String fileName, {
/* to avoid parsing incorrect date time from the filename, the max and min
year limits the chances of parsing incorrect date times
*/
int minYear = 1990,
int? maxYear,
}) {
// add next year to avoid corner cases for 31st Dec
maxYear ??= currentYear + 1;
String val = fileName.replaceAll(exp, '');
if (val.isNotEmpty && !isNumeric(val[0])) {
val = val.substring(1, val.length);
@ -319,7 +307,10 @@ DateTime? parseDateTimeFromFileNameV2(String fileName) {
if (kDebugMode && result == null) {
debugPrint("Failed to parse $fileName dateTime from $valForParser");
}
return result;
if (result != null && result.year >= minYear && result.year <= maxYear) {
return result;
}
return null;
}
bool isNumeric(String? s) {

View file

@ -153,7 +153,7 @@ class FileUploader {
}
return CollectionsService.instance
.addToCollection(collectionID, [uploadedFile]).then((aVoid) {
return uploadedFile;
return uploadedFile as File;
});
});
}

View file

@ -9,14 +9,14 @@ void main() {
"IMG-20221109-WA0000",
'''Screenshot_20220807-195908_Firefox''',
'''Screenshot_20220507-195908''',
"2019-02-18 16.00.12-DCMX.png",
"2022-02-18 16.00.12-DCMX.png",
"20221107_231730",
"2020-11-01 02.31.02",
"IMG_20210921_144423",
"2019-10-31 155703",
"IMG_20210921_144423_783",
"Screenshot_2022-06-21-16-51-29-164_newFormat.heic",
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg"
"Screenshot 20221106 211633.com.google.android.apps.nbu.paisa.user.jpg",
];
for (String val in validParsing) {
final parsedValue = parseDateTimeFromFileNameV2(val);
@ -31,6 +31,21 @@ void main() {
}
});
test("test invalid datetime parsing", () {
final List<String> badParsing = ["Snapchat-431959199.mp4."];
for (String val in badParsing) {
final parsedValue = parseDateTimeFromFileNameV2(val);
expect(
parsedValue == null,
true,
reason: "parsing should have failed $val",
);
if (kDebugMode) {
debugPrint("Parsed $val as ${parsedValue?.toIso8601String()}");
}
}
});
test("verify constants", () {
final date = DateTime.fromMicrosecondsSinceEpoch(jan011981Time).toUtc();
expect(