Skip to content
This repository was archived by the owner on Sep 14, 2024. It is now read-only.

Commit feed63e

Browse files
committed
add login via qr code
1 parent cd92f95 commit feed63e

File tree

8 files changed

+151
-4
lines changed

8 files changed

+151
-4
lines changed

android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ android {
3636
defaultConfig {
3737
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
3838
applicationId "com.nextcloud_cookbook_flutter"
39-
minSdkVersion 19
39+
minSdkVersion 20
4040
targetSdkVersion flutter.targetSdkVersion
4141
versionCode flutterVersionCode.toInteger()
4242
versionName flutterVersionName

lib/src/blocs/login/login_bloc.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:nextcloud/nextcloud.dart';
77
import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart';
88
import 'package:nextcloud_cookbook_flutter/src/models/app_authentication.dart';
99
import 'package:nextcloud_cookbook_flutter/src/services/services.dart';
10+
import 'package:nextcloud_cookbook_flutter/src/util/nextcloud_login_qr_util.dart';
1011
import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart';
1112

1213
part 'login_event.dart';
@@ -17,6 +18,7 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
1718
required this.authenticationBloc,
1819
}) : super(const LoginState()) {
1920
on<LoginFlowStart>(_mapLoginFlowStartEventToState);
21+
on<LoginQRScenned>(_mapLoginQRScannedEventToState);
2022
}
2123
final UserRepository userRepository = UserRepository();
2224
final AuthenticationBloc authenticationBloc;
@@ -54,4 +56,27 @@ class LoginBloc extends Bloc<LoginEvent, LoginState> {
5456
emit(LoginState(status: LoginStatus.failure, error: e.toString()));
5557
}
5658
}
59+
60+
Future<void> _mapLoginQRScannedEventToState(
61+
LoginQRScenned event,
62+
Emitter<LoginState> emit,
63+
) async {
64+
assert(event.uri.isScheme('nc'));
65+
try {
66+
final auth = parseNCLoginQR(event.uri);
67+
68+
authenticationBloc.add(
69+
LoggedIn(
70+
appAuthentication: AppAuthentication(
71+
server: auth['server']!,
72+
loginName: auth['user']!,
73+
appPassword: auth['password']!,
74+
isSelfSignedCertificate: false,
75+
),
76+
),
77+
);
78+
} catch (e) {
79+
emit(LoginState(status: LoginStatus.failure, error: e.toString()));
80+
}
81+
}
5782
}

lib/src/blocs/login/login_event.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,9 @@ class LoginFlowStart extends LoginEvent {
1212

1313
final String serverURL;
1414
}
15+
16+
class LoginQRScenned extends LoginEvent {
17+
const LoginQRScenned(this.uri);
18+
19+
final Uri uri;
20+
}

lib/src/screens/login_qr_screen.dart

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import 'dart:io';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:qr_code_scanner/qr_code_scanner.dart';
5+
6+
class LoginQrScreen extends StatefulWidget {
7+
const LoginQrScreen({super.key});
8+
9+
@override
10+
State<LoginQrScreen> createState() => _LoginQrScreenState();
11+
}
12+
13+
class _LoginQrScreenState extends State<LoginQrScreen> {
14+
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
15+
Barcode? result;
16+
QRViewController? controller;
17+
18+
@override
19+
void reassemble() {
20+
super.reassemble();
21+
if (Platform.isAndroid) {
22+
controller!.pauseCamera();
23+
} else if (Platform.isIOS) {
24+
controller!.resumeCamera();
25+
}
26+
}
27+
28+
@override
29+
Widget build(BuildContext context) {
30+
final theme = Theme.of(context);
31+
32+
return Scaffold(
33+
appBar: AppBar(),
34+
body: QRView(
35+
formatsAllowed: const [
36+
BarcodeFormat.qrcode,
37+
],
38+
overlay: QrScannerOverlayShape(
39+
borderColor: theme.colorScheme.primaryContainer,
40+
borderWidth: 15,
41+
borderRadius: 10,
42+
),
43+
key: qrKey,
44+
onQRViewCreated: _onQRViewCreated,
45+
),
46+
);
47+
}
48+
49+
void _onQRViewCreated(QRViewController controller) {
50+
this.controller = controller;
51+
controller.scannedDataStream.listen((scanData) {
52+
final code = scanData.code;
53+
if (code != null && code.isNotEmpty) {
54+
final uri = Uri.tryParse(code);
55+
if (uri != null && uri.isScheme('nc')) {
56+
Navigator.of(context).pop(uri);
57+
}
58+
}
59+
});
60+
}
61+
62+
@override
63+
void dispose() {
64+
controller?.dispose();
65+
super.dispose();
66+
}
67+
}

lib/src/screens/login_screen.dart

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter_translate/flutter_translate.dart';
77
import 'package:nextcloud_cookbook_flutter/src/blocs/authentication/authentication_bloc.dart';
88

99
import 'package:nextcloud_cookbook_flutter/src/blocs/login/login_bloc.dart';
10+
import 'package:nextcloud_cookbook_flutter/src/screens/login_qr_screen.dart';
1011
import 'package:nextcloud_cookbook_flutter/src/util/url_validator.dart';
1112
import 'package:vector_graphics/vector_graphics.dart';
1213
import 'package:webview_flutter/webview_flutter.dart';
@@ -41,6 +42,16 @@ class _LoginScreenState extends State<LoginScreen> {
4142
super.didChangeDependencies();
4243
}
4344

45+
Future<void> _authenticateQR() async {
46+
final uri = await Navigator.of(context).push<Uri>(
47+
MaterialPageRoute(builder: (_) => const LoginQrScreen()),
48+
);
49+
50+
if (uri != null) {
51+
_loginBloc.add(LoginQRScenned(uri));
52+
}
53+
}
54+
4455
void onSubmit([String? value]) {
4556
_formKey.currentState!.save();
4657
}
@@ -103,9 +114,9 @@ class _LoginScreenState extends State<LoginScreen> {
103114
onSaved: submit,
104115
),
105116
),
106-
const IconButton(
107-
onPressed: null,
108-
icon: Icon(
117+
IconButton(
118+
onPressed: _authenticateQR,
119+
icon: const Icon(
109120
Icons.qr_code_scanner,
110121
size: 40,
111122
),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// Parses the content of a LoginQr
2+
///
3+
/// The result will be like:
4+
/// ```dart
5+
/// {
6+
/// 'user': 'admin',
7+
/// 'password': 'superSecret',
8+
/// 'server': 'https://example.com',
9+
/// }
10+
/// ```
11+
Map<String, String> parseNCLoginQR(Uri uri) {
12+
return uri.path.split('&').map((e) {
13+
final parts = e.split(':');
14+
final key = parts[0].replaceFirst(RegExp('/'), '');
15+
parts.removeAt(0);
16+
return {key: parts.join(':')};
17+
}).fold({}, (p, e) => p..addAll(e));
18+
}

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ dependencies:
104104
flutter_native_splash: ^2.2.19
105105
webview_flutter: ^4.0.7
106106
vector_graphics: ^1.1.4
107+
qr_code_scanner: ^1.0.1
107108

108109
dev_dependencies:
109110
flutter_launcher_icons: ^0.12.0
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:nextcloud_cookbook_flutter/src/util/nextcloud_login_qr_util.dart';
3+
4+
void main() {
5+
const username = 'admin';
6+
const password = 'superSecret';
7+
const server = 'https://example.com';
8+
9+
final content =
10+
Uri.parse('nc://login/user:$username&password:$password&server:$server');
11+
12+
test('parseNCLoginQR', () {
13+
expect(parseNCLoginQR(content), {
14+
'user': username,
15+
'password': password,
16+
'server': server,
17+
});
18+
});
19+
}

0 commit comments

Comments
 (0)