Browse Source

Add import & export accounts page added

Sasan Salamzadeh 1 year ago
parent
commit
dbbf9205f6

+ 3 - 1
lib/l10n/app_en.arb

@@ -48,5 +48,7 @@
     "transferCodesTitle": "Transfer codes",
     "transferCodesDescription": "Transfer your accounts to another device.",
     "exportAccounts": "Export accounts",
-    "importAccounts": "Import accounts"
+    "importAccounts": "Import accounts",
+    "importSuccess": "Accounts imported successfully!",
+    "importDescription": "Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code."
 }

+ 12 - 0
lib/l10n/app_localizations.dart

@@ -393,6 +393,18 @@ abstract class AppLocalizations {
   /// In en, this message translates to:
   /// **'Import accounts'**
   String get importAccounts;
+
+  /// No description provided for @importSuccess.
+  ///
+  /// In en, this message translates to:
+  /// **'Accounts imported successfully!'**
+  String get importSuccess;
+
+  /// No description provided for @importDescription.
+  ///
+  /// In en, this message translates to:
+  /// **'Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.'**
+  String get importDescription;
 }
 
 class _AppLocalizationsDelegate

+ 7 - 0
lib/l10n/app_localizations_en.dart

@@ -165,4 +165,11 @@ class AppLocalizationsEn extends AppLocalizations {
 
   @override
   String get importAccounts => 'Import accounts';
+
+  @override
+  String get importSuccess => 'Accounts imported successfully!';
+
+  @override
+  String get importDescription =>
+      'Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.';
 }

+ 125 - 0
lib/pages/export_accounts.dart

@@ -0,0 +1,125 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import '../state/app_state.dart';
+import 'package:pb_authenticator_totp/totp_item.dart';
+import 'package:pb_authenticator_totp/otpauth_migration.dart';
+import 'package:barcode_scan2/barcode_scan2.dart';
+import '../l10n/app_localizations.dart';
+
+class ExportAccountsPage extends StatefulWidget {
+  const ExportAccountsPage({super.key});
+
+  @override
+  State<ExportAccountsPage> createState() => _ExportAccountsPageState();
+}
+
+class _ExportAccountsPageState extends State<ExportAccountsPage> {
+  static const int maxAccountsPerBatch = 10; // Google Authenticator uses 10
+  List<String> _qrUris = [];
+  int _currentIndex = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    WidgetsBinding.instance.addPostFrameCallback((_) => _generateQrUris());
+  }
+
+  void _generateQrUris() {
+    final state = Provider.of<AppState>(context, listen: false);
+    final items = state.items ?? [];
+    final otpAuths = items.map((item) => _toOtpAuthUri(item.totp)).toList();
+    _qrUris = [];
+    for (int i = 0; i < otpAuths.length; i += maxAccountsPerBatch) {
+      final batch = otpAuths.sublist(i, (i + maxAccountsPerBatch > otpAuths.length) ? otpAuths.length : i + maxAccountsPerBatch);
+      final uri = OtpAuthMigration().encode(batch, batchSize: otpAuths.length, batchIndex: (i ~/ maxAccountsPerBatch), batchId: 1);
+      _qrUris.add(uri);
+    }
+    setState(() {});
+  }
+
+  String _toOtpAuthUri(TotpItem item) {
+    final algorithm = item.algorithm.name.toUpperCase();
+    return 'otpauth://totp/${Uri.encodeComponent(item.issuer)}:${Uri.encodeComponent(item.accountName)}?secret=${item.secret}&issuer=${Uri.encodeComponent(item.issuer)}&algorithm=$algorithm&digits=${item.digits}&period=${item.period}';
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_qrUris.isEmpty) {
+      final state = Provider.of<AppState>(context, listen: false);
+      final items = state.items ?? [];
+      if (items.isEmpty) {
+        return Scaffold(
+          appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
+          body: Center(child: Text(AppLocalizations.of(context)?.noAccounts ?? 'No accounts to export.')),
+        );
+      }
+      // Still loading QR codes
+      return Scaffold(
+        appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
+        body: const Center(child: CircularProgressIndicator()),
+      );
+    }
+    return Scaffold(
+      appBar: AppBar(title: Text(AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts')),
+      body: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Text(
+            AppLocalizations.of(context)?.exportAccounts ?? 'Export accounts',
+            style: Theme.of(context).textTheme.titleLarge,
+          ),
+          const SizedBox(height: 16),
+          Expanded(
+            child: Center(
+              child: BarcodePainterWidget(data: _qrUris[_currentIndex]),
+            ),
+          ),
+          if (_qrUris.length > 1)
+            Padding(
+              padding: const EdgeInsets.symmetric(vertical: 8.0),
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  IconButton(
+                    icon: const Icon(Icons.arrow_back),
+                    onPressed: _currentIndex > 0
+                        ? () => setState(() => _currentIndex--)
+                        : null,
+                  ),
+                  Text('${_currentIndex + 1} / ${_qrUris.length}'),
+                  IconButton(
+                    icon: const Icon(Icons.arrow_forward),
+                    onPressed: _currentIndex < _qrUris.length - 1
+                        ? () => setState(() => _currentIndex++)
+                        : null,
+                  ),
+                ],
+              ),
+            ),
+          const SizedBox(height: 16),
+        ],
+      ),
+    );
+  }
+}
+
+class BarcodePainterWidget extends StatelessWidget {
+  final String data;
+  const BarcodePainterWidget({super.key, required this.data});
+
+  @override
+  Widget build(BuildContext context) {
+    // barcode_scan2 does not provide a direct widget, so we use a placeholder for now.
+    // You should replace this with a QR code widget compatible with your requirements.
+    return Container(
+      color: Colors.white,
+      width: 240,
+      height: 240,
+      alignment: Alignment.center,
+      child: Text(
+        'QR code here',
+        style: TextStyle(color: Colors.black),
+      ),
+    );
+  }
+} 

+ 96 - 0
lib/pages/import_accounts.dart

@@ -0,0 +1,96 @@
+import 'package:flutter/material.dart';
+import 'package:barcode_scan2/barcode_scan2.dart';
+import 'package:pb_authenticator_totp/otpauth_migration.dart';
+import 'package:pb_authenticator_totp/totp_item.dart';
+import 'package:provider/provider.dart';
+import '../state/app_state.dart';
+import '../l10n/app_localizations.dart';
+
+class ImportAccountsPage extends StatefulWidget {
+  const ImportAccountsPage({super.key});
+
+  @override
+  State<ImportAccountsPage> createState() => _ImportAccountsPageState();
+}
+
+class _ImportAccountsPageState extends State<ImportAccountsPage> {
+  bool _importing = false;
+  String? _resultMessage;
+
+  Future<void> _scanAndImport() async {
+    setState(() {
+      _importing = true;
+      _resultMessage = null;
+    });
+    try {
+      final scanResult = await BarcodeScanner.scan();
+      final uri = scanResult.rawContent;
+      if (uri.isEmpty) {
+        setState(() {
+          _importing = false;
+          _resultMessage = AppLocalizations.of(context)?.errIncorrectFormat ?? 'Scanned code is of incorrect format';
+        });
+        return;
+      }
+      final uris = OtpAuthMigration().decode(uri);
+      if (uris.isEmpty) {
+        setState(() {
+          _importing = false;
+          _resultMessage = AppLocalizations.of(context)?.errIncorrectFormat ?? 'Scanned code is of incorrect format';
+        });
+        return;
+      }
+      final items = uris.map((u) => TotpItem.fromUri(u)).toList();
+      final state = Provider.of<AppState>(context, listen: false);
+      for (final item in items) {
+        await state.addItem(item);
+      }
+      setState(() {
+        _importing = false;
+        _resultMessage = AppLocalizations.of(context)?.importSuccess ?? 'Accounts imported successfully!';
+      });
+    } catch (e) {
+      setState(() {
+        _importing = false;
+        _resultMessage = AppLocalizations.of(context)?.errUnknown ?? 'Unknown error';
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(title: Text(AppLocalizations.of(context)?.importAccounts ?? 'Import accounts')),
+      body: Center(
+        child: _importing
+            ? const CircularProgressIndicator()
+            : Column(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Icon(Icons.qr_code_2, size: 64, color: Colors.blue),
+                  const SizedBox(height: 16),
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 24.0),
+                    child: Text(
+                      AppLocalizations.of(context)?.importDescription ??
+                          'Import your accounts from Google Authenticator or PB Authenticator by scanning a QR code.',
+                      textAlign: TextAlign.center,
+                      style: Theme.of(context).textTheme.bodyMedium,
+                    ),
+                  ),
+                  const SizedBox(height: 32),
+                  ElevatedButton.icon(
+                    icon: const Icon(Icons.qr_code_scanner),
+                    label: Text(AppLocalizations.of(context)?.importAccounts ?? 'Import accounts'),
+                    onPressed: _scanAndImport,
+                  ),
+                  if (_resultMessage != null) ...[
+                    const SizedBox(height: 24),
+                    Text(_resultMessage!, textAlign: TextAlign.center),
+                  ]
+                ],
+              ),
+      ),
+    );
+  }
+} 

+ 8 - 2
lib/pages/transfer_codes.dart

@@ -1,5 +1,7 @@
 import 'package:flutter/material.dart';
 import '../l10n/app_localizations.dart';
+import 'export_accounts.dart';
+import 'import_accounts.dart';
 
 class TransferCodesPage extends StatelessWidget {
   const TransferCodesPage({super.key});
@@ -35,7 +37,9 @@ class TransferCodesPage extends StatelessWidget {
                 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
               ),
               onPressed: () {
-                // TODO: Navigate to export page
+                Navigator.of(context).push(
+                  MaterialPageRoute(builder: (context) => const ExportAccountsPage()),
+                );
               },
             ),
             const SizedBox(height: 24),
@@ -53,7 +57,9 @@ class TransferCodesPage extends StatelessWidget {
                 shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
               ),
               onPressed: () {
-                // TODO: Navigate to import page
+                Navigator.of(context).push(
+                  MaterialPageRoute(builder: (context) => const ImportAccountsPage()),
+                );
               },
             ),
           ],