feat: add pinning
This commit is contained in:
parent
c1103b656c
commit
b516bc8a52
17 changed files with 212 additions and 49 deletions
|
@ -157,6 +157,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"invalidQRCode": "Invalid QR code",
|
||||
"noRecoveryKeyTitle": "No recovery key?",
|
||||
"enterEmailHint": "Enter your email address",
|
||||
"invalidEmailTitle": "Invalid email address",
|
||||
|
@ -421,5 +422,6 @@
|
|||
"invalidEndpoint": "Invalid endpoint",
|
||||
"invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.",
|
||||
"endpointUpdatedMessage": "Endpoint updated successfully",
|
||||
"customEndpoint": "Connected to {endpoint}"
|
||||
"customEndpoint": "Connected to {endpoint}",
|
||||
"pinText": "Pin"
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:ente_auth/models/code_display.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Code {
|
||||
static const defaultDigits = 6;
|
||||
|
@ -12,10 +14,16 @@ class Code {
|
|||
final String secret;
|
||||
final Algorithm algorithm;
|
||||
final Type type;
|
||||
|
||||
/// otpauth url in the code
|
||||
final String rawData;
|
||||
final int counter;
|
||||
bool? hasSynced;
|
||||
|
||||
final CodeDisplay? display;
|
||||
|
||||
bool get isPinned => display?.pinned ?? false;
|
||||
|
||||
Code(
|
||||
this.account,
|
||||
this.issuer,
|
||||
|
@ -27,6 +35,7 @@ class Code {
|
|||
this.counter,
|
||||
this.rawData, {
|
||||
this.generatedID,
|
||||
this.display,
|
||||
});
|
||||
|
||||
Code copyWith({
|
||||
|
@ -38,6 +47,7 @@ class Code {
|
|||
Algorithm? algorithm,
|
||||
Type? type,
|
||||
int? counter,
|
||||
CodeDisplay? display,
|
||||
}) {
|
||||
final String updateAccount = account ?? this.account;
|
||||
final String updateIssuer = issuer ?? this.issuer;
|
||||
|
@ -47,6 +57,7 @@ class Code {
|
|||
final Algorithm updatedAlgo = algorithm ?? this.algorithm;
|
||||
final Type updatedType = type ?? this.type;
|
||||
final int updatedCounter = counter ?? this.counter;
|
||||
final CodeDisplay? updatedDisplay = display ?? this.display;
|
||||
|
||||
return Code(
|
||||
updateAccount,
|
||||
|
@ -59,6 +70,7 @@ class Code {
|
|||
updatedCounter,
|
||||
"otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
|
||||
generatedID: generatedID,
|
||||
display: updatedDisplay,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -80,7 +92,7 @@ class Code {
|
|||
);
|
||||
}
|
||||
|
||||
static Code fromRawData(String rawData) {
|
||||
static Code fromOTPAuthUrl(String rawData, {CodeDisplay? display}) {
|
||||
Uri uri = Uri.parse(rawData);
|
||||
try {
|
||||
return Code(
|
||||
|
@ -98,7 +110,7 @@ class Code {
|
|||
// if account name contains # without encoding,
|
||||
// rest of the url are treated as url fragment
|
||||
if (rawData.contains("#")) {
|
||||
return Code.fromRawData(rawData.replaceAll("#", '%23'));
|
||||
return Code.fromOTPAuthUrl(rawData.replaceAll("#", '%23'));
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
|
@ -122,6 +134,26 @@ class Code {
|
|||
}
|
||||
}
|
||||
|
||||
static Code fromExportJson(Map rawJson) {
|
||||
try {
|
||||
Code resultCode = Code.fromOTPAuthUrl(
|
||||
rawJson['rawData'],
|
||||
display: CodeDisplay.fromJson(rawJson['display']),
|
||||
);
|
||||
return resultCode;
|
||||
} catch (e) {
|
||||
debugPrint("Failed to parse code from export json $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toExportJson() {
|
||||
return {
|
||||
'rawData': rawData,
|
||||
'display': display?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
static String _getIssuer(Uri uri) {
|
||||
try {
|
||||
if (uri.queryParameters.containsKey("issuer")) {
|
||||
|
@ -184,7 +216,7 @@ class Code {
|
|||
}
|
||||
|
||||
static Type _getType(Uri uri) {
|
||||
if (uri.host == "totp") {
|
||||
if (uri.host == "totp" || uri.host == "steam") {
|
||||
return Type.totp;
|
||||
} else if (uri.host == "hotp") {
|
||||
return Type.hotp;
|
||||
|
@ -216,7 +248,8 @@ class Code {
|
|||
secret.hashCode ^
|
||||
type.hashCode ^
|
||||
counter.hashCode ^
|
||||
rawData.hashCode;
|
||||
rawData.hashCode ^
|
||||
display.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
54
auth/lib/models/code_display.dart
Normal file
54
auth/lib/models/code_display.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
/// Used to store the display settings of a code.
|
||||
class CodeDisplay {
|
||||
final bool pinned;
|
||||
final bool trashed;
|
||||
final int lastUsedAt;
|
||||
final int tapCount;
|
||||
|
||||
CodeDisplay({
|
||||
this.pinned = false,
|
||||
this.trashed = false,
|
||||
this.lastUsedAt = 0,
|
||||
this.tapCount = 0,
|
||||
});
|
||||
|
||||
// copyWith
|
||||
CodeDisplay copyWith({
|
||||
bool? pinned,
|
||||
bool? trashed,
|
||||
int? lastUsedAt,
|
||||
int? tapCount,
|
||||
}) {
|
||||
final bool updatedPinned = pinned ?? this.pinned;
|
||||
final bool updatedTrashed = trashed ?? this.trashed;
|
||||
final int updatedLastUsedAt = lastUsedAt ?? this.lastUsedAt;
|
||||
final int updatedTapCount = tapCount ?? this.tapCount;
|
||||
return CodeDisplay(
|
||||
pinned: updatedPinned,
|
||||
trashed: updatedTrashed,
|
||||
lastUsedAt: updatedLastUsedAt,
|
||||
tapCount: updatedTapCount,
|
||||
);
|
||||
}
|
||||
|
||||
factory CodeDisplay.fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return CodeDisplay();
|
||||
}
|
||||
return CodeDisplay(
|
||||
pinned: json['pinned'] ?? false,
|
||||
trashed: json['trashed'] ?? false,
|
||||
lastUsedAt: json['lastUsedAt'] ?? 0,
|
||||
tapCount: json['tapCount'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'pinned': pinned,
|
||||
'trashed': trashed,
|
||||
'lastUsedAt': lastUsedAt,
|
||||
'tapCount': tapCount,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -29,10 +29,17 @@ class CodeStore {
|
|||
final List<Code> codes = [];
|
||||
for (final entity in entities) {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
final code = Code.fromRawData(decodeJson);
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
if (decodeJson is String && decodeJson.startsWith('otpauth://')) {
|
||||
final code = Code.fromOTPAuthUrl(decodeJson);
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
} else {
|
||||
final code = Code.fromExportJson(decodeJson);
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
}
|
||||
}
|
||||
|
||||
// sort codes by issuer,account
|
||||
|
@ -54,28 +61,33 @@ class CodeStore {
|
|||
final mode = accountMode ?? _authenticatorService.getAccountMode();
|
||||
final codes = await getAllCodes(accountMode: mode);
|
||||
bool isExistingCode = false;
|
||||
bool hasSameCode = false;
|
||||
for (final existingCode in codes) {
|
||||
if (existingCode == code) {
|
||||
_logger.info("Found duplicate code, skipping add");
|
||||
return AddResult.duplicate;
|
||||
} else if (existingCode.generatedID == code.generatedID) {
|
||||
if (code.generatedID != null &&
|
||||
existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
break;
|
||||
}
|
||||
if (existingCode == code) {
|
||||
hasSameCode = true;
|
||||
}
|
||||
}
|
||||
if (!isExistingCode && hasSameCode) {
|
||||
return AddResult.duplicate;
|
||||
}
|
||||
late AddResult result;
|
||||
if (isExistingCode) {
|
||||
result = AddResult.updateCode;
|
||||
await _authenticatorService.updateEntry(
|
||||
code.generatedID!,
|
||||
jsonEncode(code.rawData),
|
||||
jsonEncode(code.toExportJson()),
|
||||
shouldSync,
|
||||
mode,
|
||||
);
|
||||
} else {
|
||||
result = AddResult.newCode;
|
||||
code.generatedID = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
jsonEncode(code.toExportJson()),
|
||||
shouldSync,
|
||||
mode,
|
||||
);
|
||||
|
@ -93,7 +105,7 @@ class CodeStore {
|
|||
bool _isOfflineImportRunning = false;
|
||||
|
||||
Future<void> importOfflineCodes() async {
|
||||
if(_isOfflineImportRunning) {
|
||||
if (_isOfflineImportRunning) {
|
||||
return;
|
||||
}
|
||||
_isOfflineImportRunning = true;
|
||||
|
|
|
@ -6,6 +6,7 @@ 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/models/code.dart';
|
||||
import 'package:ente_auth/models/code_display.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/onboarding/view/view_qr_page.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
|
@ -97,6 +98,13 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.pinText,
|
||||
icon: widget.code.isPinned
|
||||
? Icons.push_pin
|
||||
: Icons.push_pin_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.edit,
|
||||
icon: Icons.edit,
|
||||
|
@ -139,6 +147,22 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onPinPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: widget.code.isPinned
|
||||
? Icons.push_pin
|
||||
: Icons.push_pin_outlined,
|
||||
label: l10n.pinText,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
|
@ -448,6 +472,15 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _onPinPressed(_) async {
|
||||
bool currentlyPinned = widget.code.isPinned;
|
||||
final display = widget.code.display ?? CodeDisplay();
|
||||
final Code code = widget.code.copyWith(
|
||||
display: display.copyWith(pinned: !currentlyPinned),
|
||||
);
|
||||
unawaited(CodeStore.instance.addCode(code));
|
||||
}
|
||||
|
||||
void _onDeletePressed(_) async {
|
||||
bool isAuthSuccessful =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
|
|
|
@ -99,6 +99,8 @@ class _HomePageState extends State<HomePage> {
|
|||
_codes = codes;
|
||||
_hasLoaded = true;
|
||||
_applyFilteringAndRefresh();
|
||||
}).onError((error, stackTrace) {
|
||||
_logger.severe('Error while loading codes', error, stackTrace);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -258,6 +260,11 @@ class _HomePageState extends State<HomePage> {
|
|||
onManuallySetupTap: _redirectToManualEntryPage,
|
||||
);
|
||||
} else {
|
||||
_filteredCodes.sort((a, b) {
|
||||
if (b.isPinned && !a.isPinned) return 1;
|
||||
if (!b.isPinned && a.isPinned) return -1;
|
||||
return 0;
|
||||
});
|
||||
final list = AlignedGridView.count(
|
||||
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
|
||||
.clamp(1, double.infinity)
|
||||
|
@ -360,7 +367,7 @@ class _HomePageState extends State<HomePage> {
|
|||
}
|
||||
if (mounted && link.toLowerCase().startsWith("otpauth://")) {
|
||||
try {
|
||||
final newCode = Code.fromRawData(link);
|
||||
final newCode = Code.fromOTPAuthUrl(link);
|
||||
getNextTotp(newCode);
|
||||
CodeStore.instance.addCode(newCode);
|
||||
_focusNewCode(newCode);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
|
@ -66,11 +67,12 @@ class ScannerPageState extends State<ScannerPage> {
|
|||
}
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
try {
|
||||
final code = Code.fromRawData(scanData.code!);
|
||||
final code = Code.fromOTPAuthUrl(scanData.code!);
|
||||
controller.dispose();
|
||||
Navigator.of(context).pop(code);
|
||||
} catch (e) {
|
||||
// Log
|
||||
showToast(context, context.l10n.invalidQRCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -172,9 +172,13 @@ Future<void> _exportCodes(BuildContext context, String fileContent) async {
|
|||
|
||||
Future<String> _getAuthDataForExport() async {
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
List<Map<String, dynamic>> items = [];
|
||||
for (final code in codes) {
|
||||
data += "${code.rawData}\n";
|
||||
items.add(code.toExportJson());
|
||||
}
|
||||
return data;
|
||||
final data = {
|
||||
"items": items,
|
||||
};
|
||||
|
||||
return jsonEncode(data);
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
import 'package:convert/convert.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
|
@ -150,7 +150,7 @@ Future<int?> _processAegisExportFile(
|
|||
} else {
|
||||
throw Exception('Invalid OTP type');
|
||||
}
|
||||
parsedCodes.add(Code.fromRawData(otpUrl));
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
|
|
|
@ -86,7 +86,7 @@ Future<int?> _processBitwardenExportFile(
|
|||
Code code;
|
||||
|
||||
if (totp.contains("otpauth://")) {
|
||||
code = Code.fromRawData(totp);
|
||||
code = Code.fromOTPAuthUrl(totp);
|
||||
} else {
|
||||
var issuer = item['name'];
|
||||
var account = item['login']['username'];
|
||||
|
|
|
@ -110,7 +110,7 @@ Future<void> _decryptExportData(
|
|||
final parsedCodes = [];
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromRawData(code));
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(code));
|
||||
} catch (e) {
|
||||
Logger('EncryptedText').severe("Could not parse code", e);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:base32/base32.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
|
@ -124,7 +125,7 @@ List<Code> parseGoogleAuth(String qrCodeData) {
|
|||
} else {
|
||||
throw Exception('Invalid OTP type');
|
||||
}
|
||||
codes.add(Code.fromRawData(otpUrl));
|
||||
codes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
}
|
||||
return codes;
|
||||
} catch (e, s) {
|
||||
|
|
|
@ -89,8 +89,8 @@ Future<int?> _processLastpassExportFile(
|
|||
|
||||
// Build the OTP URL
|
||||
String otpUrl =
|
||||
'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
|
||||
parsedCodes.add(Code.fromRawData(otpUrl));
|
||||
'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
|
|
|
@ -101,20 +101,35 @@ Future<void> _pickImportFile(BuildContext context) async {
|
|||
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
|
||||
await progressDialog.show();
|
||||
try {
|
||||
final parsedCodes = [];
|
||||
File file = File(result.files.single.path!);
|
||||
final codes = await file.readAsString();
|
||||
List<String> splitCodes = codes.split(",");
|
||||
if (splitCodes.length == 1) {
|
||||
splitCodes = const LineSplitter().convert(codes);
|
||||
}
|
||||
final parsedCodes = [];
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromRawData(code));
|
||||
} catch (e) {
|
||||
Logger('PlainText').severe("Could not parse code", e);
|
||||
|
||||
if (codes.startsWith('otpauth://')) {
|
||||
List<String> splitCodes = codes.split(",");
|
||||
if (splitCodes.length == 1) {
|
||||
splitCodes = codes.split("\n");
|
||||
}
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(code));
|
||||
} catch (e) {
|
||||
Logger('PlainText').severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final decodedCodes = jsonDecode(codes);
|
||||
List<Map> splitCodes = List.from(decodedCodes["items"]);
|
||||
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromExportJson(code));
|
||||
} catch (e) {
|
||||
Logger('PlainText').severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ Future<void> _pickRaivoJsonFile(BuildContext context) async {
|
|||
String path = result.files.single.path!;
|
||||
int? count = await _processRaivoExportFile(context, path);
|
||||
await progressDialog.hide();
|
||||
if(count != null) {
|
||||
if (count != null) {
|
||||
await importSuccessDialog(context, count);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -70,9 +70,9 @@ Future<void> _pickRaivoJsonFile(BuildContext context) async {
|
|||
}
|
||||
}
|
||||
|
||||
Future<int?> _processRaivoExportFile(BuildContext context,String path) async {
|
||||
Future<int?> _processRaivoExportFile(BuildContext context, String path) async {
|
||||
File file = File(path);
|
||||
if(path.endsWith('.zip')) {
|
||||
if (path.endsWith('.zip')) {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.sorry,
|
||||
|
@ -105,7 +105,7 @@ Future<int?> _processRaivoExportFile(BuildContext context,String path) async {
|
|||
} else {
|
||||
throw Exception('Invalid OTP type');
|
||||
}
|
||||
parsedCodes.add(Code.fromRawData(otpUrl));
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
|
|
|
@ -158,7 +158,7 @@ Future<int?> _process2FasExportFile(
|
|||
} else {
|
||||
throw Exception('Invalid OTP type');
|
||||
}
|
||||
parsedCodes.add(Code.fromRawData(otpUrl));
|
||||
parsedCodes.add(Code.fromOTPAuthUrl(otpUrl));
|
||||
}
|
||||
|
||||
for (final code in parsedCodes) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
|
||||
void main() {
|
||||
test("parseCodeFromRawData", () {
|
||||
final code1 = Code.fromRawData(
|
||||
final code1 = Code.fromOTPAuthUrl(
|
||||
"otpauth://totp/example%20finance%3Aee%40ff.gg?secret=ASKZNWOU6SVYAMVS",
|
||||
);
|
||||
expect(code1.issuer, "example finance", reason: "issuerMismatch");
|
||||
|
@ -12,7 +12,7 @@ void main() {
|
|||
});
|
||||
|
||||
test("parseDocumentedFormat", () {
|
||||
final code = Code.fromRawData(
|
||||
final code = Code.fromOTPAuthUrl(
|
||||
"otpauth://totp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub",
|
||||
);
|
||||
expect(code.issuer, "GitHub", reason: "issuerMismatch");
|
||||
|
@ -21,7 +21,7 @@ void main() {
|
|||
});
|
||||
|
||||
test("validateCount", () {
|
||||
final code = Code.fromRawData(
|
||||
final code = Code.fromOTPAuthUrl(
|
||||
"otpauth://hotp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub&counter=15",
|
||||
);
|
||||
expect(code.issuer, "GitHub", reason: "issuerMismatch");
|
||||
|
@ -32,7 +32,7 @@ void main() {
|
|||
//
|
||||
|
||||
test("parseWithFunnyAccountName", () {
|
||||
final code = Code.fromRawData(
|
||||
final code = Code.fromOTPAuthUrl(
|
||||
"otpauth://totp/Mongo Atlas:Acc !@#444?algorithm=sha1&digits=6&issuer=Mongo Atlas&period=30&secret=NI4CTTFEV4G2JFE6",
|
||||
);
|
||||
expect(code.issuer, "Mongo Atlas", reason: "issuerMismatch");
|
||||
|
@ -43,11 +43,11 @@ void main() {
|
|||
test("parseAndUpdateInChinese", () {
|
||||
const String rubberDuckQr =
|
||||
'otpauth://totp/%E6%A9%A1%E7%9A%AE%E9%B8%AD?secret=2CWDCK4EOIN5DJDRMYUMYBBO4MKSR5AX&issuer=ente.io';
|
||||
final code = Code.fromRawData(rubberDuckQr);
|
||||
final code = Code.fromOTPAuthUrl(rubberDuckQr);
|
||||
expect(code.account, '橡皮鸭');
|
||||
final String updatedRawCode =
|
||||
code.copyWith(account: '伍迪', issuer: '鸭子').rawData;
|
||||
final updateCode = Code.fromRawData(updatedRawCode);
|
||||
final updateCode = Code.fromOTPAuthUrl(updatedRawCode);
|
||||
expect(updateCode.account, '伍迪', reason: 'updated accountMismatch');
|
||||
expect(updateCode.issuer, '鸭子', reason: 'updated issuerMismatch');
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue