mirror of
https://github.com/ente-io/ente.git
synced 2025-08-08 07:28:26 +00:00
[auth] Add hook to share code
This commit is contained in:
parent
af9e865745
commit
9ccb597e6e
@ -118,6 +118,7 @@
|
||||
"emailVerificationToggle": "Email verification",
|
||||
"emailVerificationEnableWarning": "To avoid getting locked out of your account, be sure to store a copy of your email 2FA outside of Ente Auth before enabling email verification.",
|
||||
"authToChangeEmailVerificationSetting": "Please authenticate to change email verification",
|
||||
"authenticateGeneric": "Please authenticate",
|
||||
"authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
|
||||
"authToChangeYourEmail": "Please authenticate to change your email",
|
||||
"authToChangeYourPassword": "Please authenticate to change your password",
|
||||
@ -211,6 +212,7 @@
|
||||
"scanAQrCode": "Scan a QR code",
|
||||
"enterDetailsManually": "Enter details manually",
|
||||
"edit": "Edit",
|
||||
"share": "Share",
|
||||
"restore": "Restore",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
"copiedNextToClipboard": "Copied next code to clipboard",
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
@ -15,6 +17,7 @@ import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/code_timer_progress.dart';
|
||||
import 'package:ente_auth/ui/share/code_share.dart';
|
||||
import 'package:ente_auth/ui/utils/icon_utils.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
@ -291,13 +294,13 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
if (!widget.code.isTrashed) SizedBox(width: slideSpace),
|
||||
if (!widget.code.isTrashed)
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
onPressed: _onSharePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
icon: Icons.adaptive.share_outlined,
|
||||
label: l10n.share,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
@ -605,6 +608,16 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSharePressed(_) async {
|
||||
bool isAuthSuccessful = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(context, context.l10n.authenticateGeneric);
|
||||
await PlatformUtil.refocusWindows();
|
||||
if (!isAuthSuccessful) {
|
||||
return;
|
||||
}
|
||||
showShareDialog(context, widget.code);
|
||||
}
|
||||
|
||||
Future<void> _onPinPressed(_) async {
|
||||
bool currentlyPinned = widget.code.isPinned;
|
||||
final display = widget.code.display;
|
||||
|
159
auth/lib/ui/share/code_share.dart
Normal file
159
auth/lib/ui/share/code_share.dart
Normal file
@ -0,0 +1,159 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/share_utils.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class ShareCodeDialog extends StatefulWidget {
|
||||
final Code code;
|
||||
const ShareCodeDialog({super.key, required this.code});
|
||||
|
||||
@override
|
||||
State<ShareCodeDialog> createState() => _ShareCodeDialogState();
|
||||
}
|
||||
|
||||
class _ShareCodeDialogState extends State<ShareCodeDialog> {
|
||||
final Logger logger = Logger('_ShareCodeDialogState');
|
||||
List<int> items = [5, 15, 30, 60];
|
||||
|
||||
String getItemLabel(int min) {
|
||||
if (min == 60) return '1 hour';
|
||||
if (min > 60) {
|
||||
var hour = '${min ~/ 60}';
|
||||
if (min % 60 == 0) return '$hour hour';
|
||||
var minx = '${min % 60}';
|
||||
return '$hour hr $minx min';
|
||||
}
|
||||
return '$min min';
|
||||
}
|
||||
|
||||
int selectedValue = 15;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Share codes'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Select the duration for which you want to share codes.',
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2(
|
||||
hint: const Text('Select an option'),
|
||||
items: items
|
||||
.map(
|
||||
(item) => DropdownMenuItem<int>(
|
||||
value: item,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(getItemLabel(item)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: selectedValue,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedValue = value ?? 15;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
buttonSize: ButtonSize.large,
|
||||
labelText: context.l10n.share,
|
||||
onTap: () async {
|
||||
try {
|
||||
await shareCode();
|
||||
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
logger.warning('Failed to share code: ${e.toString()}');
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
buttonSize: ButtonSize.large,
|
||||
labelText: context.l10n.cancel,
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> shareCode() async {
|
||||
final result = generateFutureTotpCodes(widget.code, 30);
|
||||
// show toast with total time taken
|
||||
Map<String, dynamic> data = {
|
||||
'startTime': result.$1,
|
||||
'step': widget.code.period,
|
||||
'codes': result.$2.join(","),
|
||||
};
|
||||
try {
|
||||
// generated 128 bit crypto secure random key
|
||||
final Uint8List key = generate256BitKey();
|
||||
Uint8List input = utf8.encode(jsonEncode(data));
|
||||
final encResult = CryptoUtil.encryptSync(input, key);
|
||||
String url =
|
||||
'https://auth.io/share?data=${uint8ListToUrlSafeBase64(encResult.encryptedData!)}&nonce=${uint8ListToUrlSafeBase64(encResult.nonce!)}#key=${uint8ListToUrlSafeBase64(key)}';
|
||||
logger.info('url: $url');
|
||||
shareText(url, context: context).ignore();
|
||||
} catch (e) {
|
||||
logger.warning('Failed to encrypt data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
String uint8ListToUrlSafeBase64(Uint8List data) {
|
||||
String base64Str = base64UrlEncode(data);
|
||||
return base64Str.replaceAll('=', '');
|
||||
}
|
||||
|
||||
Uint8List generate256BitKey() {
|
||||
final random = Random.secure();
|
||||
final bytes = Uint8List(32); // 32 bytes = 32 * 8 bits = 256 bits
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = random
|
||||
.nextInt(256); // Generates a random number between 0 and 255 (1 byte)
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
void showShareDialog(BuildContext context, Code code) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ShareCodeDialog(
|
||||
code: code,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -5,6 +5,8 @@ import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/dialog_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
Future<void> shareDialog(
|
||||
BuildContext context,
|
||||
@ -49,3 +51,52 @@ Future<void> shareDialog(
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Rect _sharePosOrigin(BuildContext? context, GlobalKey? key) {
|
||||
late final Rect rect;
|
||||
if (context != null) {
|
||||
rect = shareButtonRect(context, key);
|
||||
} else {
|
||||
rect = const Offset(20.0, 20.0) & const Size(10, 10);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
/// Returns the rect of button if context and key are not null
|
||||
/// If key is null, returned rect will be at the center of the screen
|
||||
Rect shareButtonRect(BuildContext context, GlobalKey? shareButtonKey) {
|
||||
Size size = MediaQuery.sizeOf(context);
|
||||
final RenderObject? renderObject =
|
||||
shareButtonKey?.currentContext?.findRenderObject();
|
||||
RenderBox? renderBox;
|
||||
if (renderObject != null && renderObject is RenderBox) {
|
||||
renderBox = renderObject;
|
||||
}
|
||||
if (renderBox == null) {
|
||||
return Rect.fromLTWH(0, 0, size.width, size.height / 2);
|
||||
}
|
||||
size = renderBox.size;
|
||||
final Offset position = renderBox.localToGlobal(Offset.zero);
|
||||
return Rect.fromCenter(
|
||||
center: position + Offset(size.width / 2, size.height / 2),
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ShareResult> shareText(
|
||||
String text, {
|
||||
BuildContext? context,
|
||||
GlobalKey? key,
|
||||
}) async {
|
||||
try {
|
||||
final sharePosOrigin = _sharePosOrigin(context, key);
|
||||
return Share.share(
|
||||
text,
|
||||
sharePositionOrigin: sharePosOrigin,
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logger("ShareUtil").severe("failed to share text", e, s);
|
||||
return ShareResult.unavailable;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user