Skip to content

Commit 0680fa9

Browse files
committed
add: webview login (allows for 2fa and other verfication)
1 parent 78af09d commit 0680fa9

8 files changed

+140
-502
lines changed

lib/client/client.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class _QuackerTwitterClient extends TwitterClient {
3030
if (response?.statusCode != null && response!.statusCode >= 200 && response.statusCode < 300) {
3131
return response;
3232
} else {
33-
return Future.error(HttpException("${response?.statusCode}: ${response?.reasonPhrase}"));
33+
print(response?.body);
34+
return Future.error(HttpException(response?.body ?? response?.statusCode.toString() ?? ""));
3435
}
3536
});
3637
}

lib/client/client_regular_account.dart

+1-371
Large diffs are not rendered by default.

lib/client/login_webview.dart

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:logging/logging.dart';
5+
import 'package:quacker/constants.dart';
6+
import 'package:quacker/database/entities.dart';
7+
import 'package:quacker/database/repository.dart';
8+
import 'package:quacker/generated/l10n.dart';
9+
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
10+
import 'package:webview_flutter/webview_flutter.dart';
11+
12+
class TwitterLoginWebview extends StatefulWidget {
13+
const TwitterLoginWebview({super.key});
14+
15+
@override
16+
State<TwitterLoginWebview> createState() => _TwitterLoginWebviewState();
17+
}
18+
19+
class _TwitterLoginWebviewState extends State<TwitterLoginWebview> {
20+
@override
21+
Widget build(BuildContext context) {
22+
WebViewPlatform.instance;
23+
final webviewCookieManager = WebviewCookieManager();
24+
final webviewController = WebViewController();
25+
webviewController.setJavaScriptMode(JavaScriptMode.unrestricted);
26+
webviewController.loadRequest(Uri.https("twitter.com", "i/flow/login"));
27+
webviewController.setNavigationDelegate(NavigationDelegate(
28+
onUrlChange: (change) async {
29+
if (change.url == "https://twitter.com/home") {
30+
final cookies = await webviewCookieManager.getCookies("https://twitter.com/i/flow/login");
31+
Logger("").info(cookies);
32+
33+
try {
34+
final expCt0 = RegExp(r'(ct0=(.+?));');
35+
final RegExpMatch? matchCt0 = expCt0.firstMatch(cookies.toString());
36+
final csrfToken = matchCt0?.group(2);
37+
if (csrfToken != null) {
38+
final Map<String, String> authHeader = {
39+
"Cookie": cookies
40+
.where((cookie) =>
41+
cookie.name == "guest_id" ||
42+
cookie.name == "gt" ||
43+
cookie.name == "att" ||
44+
cookie.name == "auth_token" ||
45+
cookie.name == "ct0")
46+
.map((cookie) => '${cookie.name}=${cookie.value}')
47+
.join(";"),
48+
"authorization": bearerToken,
49+
"x-csrf-token": csrfToken,
50+
};
51+
52+
print(authHeader);
53+
54+
final database = await Repository.writable();
55+
database.insert(tableAccounts,
56+
Account(id: csrfToken, name: "", description: "", authHeader: json.encode(authHeader)).toMap());
57+
database.close();
58+
}
59+
Navigator.pop(context);
60+
} catch (e) {
61+
throw Exception(e);
62+
}
63+
}
64+
},
65+
));
66+
return Scaffold(
67+
appBar: AppBar(
68+
toolbarHeight: 50,
69+
),
70+
body: WebViewWidget(
71+
controller: webviewController,
72+
),
73+
);
74+
}
75+
}

lib/database/entities.dart

+6-6
Original file line numberDiff line numberDiff line change
@@ -213,17 +213,17 @@ class SubscriptionGroupMember with ToMappable {
213213

214214
class Account {
215215
final String id;
216-
final String password;
217-
final String? email;
216+
final String name;
217+
final String? description;
218218
final dynamic authHeader;
219219

220-
Account({required this.id, required this.password, this.email, required this.authHeader});
220+
Account({required this.id, required this.name, this.description, required this.authHeader});
221221

222222
factory Account.fromMap(Map<String, Object?> map) {
223223
return Account(
224224
id: map['id'] as String,
225-
password: map['password'] as String,
226-
email: map['email'] as String?,
225+
name: map['name'] as String,
226+
description: map['description'] as String?,
227227
authHeader: map['auth_header'],
228228
);
229229
}
@@ -237,6 +237,6 @@ class Account {
237237

238238
@override
239239
Map<String, dynamic> toMap() {
240-
return {'screen_name': id, 'password': password, 'email': email, 'auth_header': authHeader};
240+
return {'id': id, 'password': name, 'email': description, 'auth_header': authHeader};
241241
}
242242
}

lib/database/repository.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ class Repository {
223223
// create table for storing twitter accounts
224224
SqlMigration(
225225
'CREATE TABLE IF NOT EXISTS $tableAccounts (id TEXT PRIMARY KEY, password TEXT, email TEXT, auth_header VARCHAR)'),
226-
]
226+
],
227227
});
228228
await openDatabase(
229229
databaseName,

lib/settings/_account.dart

+13-123
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import 'package:flutter/material.dart';
2+
import 'package:pref/pref.dart';
23
import 'package:quacker/client/client_regular_account.dart';
4+
import 'package:quacker/client/login_webview.dart';
35
import 'package:quacker/generated/l10n.dart';
4-
import 'package:provider/provider.dart';
5-
import 'package:pref/pref.dart';
6-
import 'package:quacker/ui/errors.dart';
76

87
class SettingsAccountFragment extends StatefulWidget {
98
const SettingsAccountFragment({super.key});
@@ -15,13 +14,12 @@ class SettingsAccountFragment extends StatefulWidget {
1514
class _SettingsAccountFragment extends State<SettingsAccountFragment> {
1615
@override
1716
Widget build(BuildContext context) {
18-
var model = context.read<XRegularAccount>();
1917
return Scaffold(
2018
appBar: AppBar(
2119
title: Text(L10n.current.account),
2220
actions: [
2321
IconButton(
24-
onPressed: () => showDialog(context: context, builder: (_) => addDialog(model)),
22+
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const TwitterLoginWebview())),
2523
icon: const Icon(Icons.add))
2624
],
2725
),
@@ -37,127 +35,19 @@ class _SettingsAccountFragment extends State<SettingsAccountFragment> {
3735
itemBuilder: (BuildContext itemContext, int index) {
3836
return Card(
3937
child: ListTile(
40-
title: Text(data[index]['id'].toString()),
41-
subtitle: Text(data[index]['email'].toString()),
42-
leading: Icon(Icons.account_circle),
43-
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
44-
IconButton(
45-
icon: Icon(Icons.refresh),
46-
onPressed: () async {
47-
await addAccount(data[index]['id'] as String, data[index]['password'] as String,
48-
data[index]['email'].toString());
49-
setState(() {});
50-
},
51-
),
52-
IconButton(
53-
icon: Icon(Icons.delete),
54-
onPressed: () async {
55-
await deleteAccount(data[index]['id'].toString());
56-
setState(() {});
57-
},
58-
)
59-
]),
60-
onTap: () => showDialog(
61-
context: context,
62-
builder: (_) => addDialog(model,
63-
username: data[index]['id'] as String,
64-
password: data[index]['password'] as String,
65-
email: data[index]['email'].toString())),
66-
));
38+
title: Text(L10n.of(context).account),
39+
subtitle: Text(L10n.of(context).unknown),
40+
leading: Icon(Icons.account_circle),
41+
trailing: IconButton(
42+
icon: Icon(Icons.delete),
43+
onPressed: () async {
44+
await deleteAccount(data[index]['id'].toString());
45+
setState(() {});
46+
},
47+
)));
6748
});
6849
}
6950
}),
7051
);
7152
}
7253
}
73-
74-
class addDialog extends StatefulWidget {
75-
final XRegularAccount model;
76-
final String username;
77-
final String password;
78-
final String email;
79-
80-
const addDialog(this.model, {super.key, this.username = "", this.password = "", this.email = ""});
81-
82-
@override
83-
State<addDialog> createState() => _addDialog();
84-
}
85-
86-
class _addDialog extends State<addDialog> {
87-
bool hidePassword = true;
88-
89-
TextEditingController _username = TextEditingController();
90-
TextEditingController _password = TextEditingController();
91-
TextEditingController _email = TextEditingController();
92-
93-
Widget build(BuildContext context) {
94-
_username.text = widget.username;
95-
_password.text = widget.password;
96-
_email.text = widget.email;
97-
98-
return AlertDialog(
99-
title: Text(L10n.of(context).account),
100-
content: Column(mainAxisSize: MainAxisSize.min, children: [
101-
Flexible(
102-
child: TextField(
103-
controller: _username,
104-
decoration: InputDecoration(
105-
isDense: true, label: Text(L10n.of(context).loginNameTwitterAcc), border: const OutlineInputBorder()),
106-
),
107-
),
108-
const SizedBox(
109-
height: 8.0,
110-
),
111-
Flexible(
112-
child: TextField(
113-
controller: _password,
114-
obscureText: hidePassword,
115-
decoration: InputDecoration(
116-
isDense: true,
117-
label: Text(L10n.of(context).passwordTwitterAcc),
118-
border: const OutlineInputBorder(),
119-
suffixIcon: IconButton(
120-
icon: Icon(hidePassword ? Icons.visibility : Icons.visibility_off),
121-
onPressed: () => setState(() => hidePassword = !hidePassword),
122-
),
123-
),
124-
),
125-
),
126-
const SizedBox(
127-
height: 8.0,
128-
),
129-
Flexible(
130-
child: TextField(
131-
controller: _email,
132-
keyboardType: TextInputType.emailAddress,
133-
decoration: InputDecoration(
134-
isDense: true, label: Text(L10n.of(context).emailTwitterAcc), border: const OutlineInputBorder()),
135-
),
136-
),
137-
const SizedBox(
138-
height: 8.0,
139-
),
140-
const Padding(
141-
padding: EdgeInsets.all(8.0),
142-
child: Text(
143-
"⚠️ 2FA is currently not supported ⚠️",
144-
textAlign: TextAlign.center,
145-
)),
146-
]),
147-
actions: [
148-
TextButton(
149-
onPressed: () async {
150-
final response = await addAccount(_username.text, _password.text, _email.text);
151-
if (context.mounted) {
152-
showSnackBar(context, icon: '', message: response);
153-
}
154-
Navigator.pop(context);
155-
156-
setState(() {});
157-
},
158-
child: Text(L10n.of(context).login)),
159-
TextButton(onPressed: () => Navigator.pop(context), child: Text(L10n.of(context).close)),
160-
],
161-
);
162-
}
163-
}

pubspec.lock

+40
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,46 @@ packages:
12841284
url: "https://pub.dev"
12851285
source: hosted
12861286
version: "0.5.1"
1287+
webview_cookie_manager:
1288+
dependency: "direct main"
1289+
description:
1290+
name: webview_cookie_manager
1291+
sha256: "425a9feac5cd2cb62a71da3dda5ac2eaf9ece5481ee8d79f3868dc5ba8223ad3"
1292+
url: "https://pub.dev"
1293+
source: hosted
1294+
version: "2.0.6"
1295+
webview_flutter:
1296+
dependency: "direct main"
1297+
description:
1298+
name: webview_flutter
1299+
sha256: "25e1b6e839e8cbfbd708abc6f85ed09d1727e24e08e08c6b8590d7c65c9a8932"
1300+
url: "https://pub.dev"
1301+
source: hosted
1302+
version: "4.7.0"
1303+
webview_flutter_android:
1304+
dependency: transitive
1305+
description:
1306+
name: webview_flutter_android
1307+
sha256: f038ee2fae73b509dde1bc9d2c5a50ca92054282de17631a9a3d515883740934
1308+
url: "https://pub.dev"
1309+
source: hosted
1310+
version: "3.16.0"
1311+
webview_flutter_platform_interface:
1312+
dependency: transitive
1313+
description:
1314+
name: webview_flutter_platform_interface
1315+
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
1316+
url: "https://pub.dev"
1317+
source: hosted
1318+
version: "2.10.0"
1319+
webview_flutter_wkwebview:
1320+
dependency: transitive
1321+
description:
1322+
name: webview_flutter_wkwebview
1323+
sha256: f12f8d8a99784b863e8b85e4a9a5e3cf1839d6803d2c0c3e0533a8f3c5a992a7
1324+
url: "https://pub.dev"
1325+
source: hosted
1326+
version: "3.13.0"
12871327
win32:
12881328
dependency: transitive
12891329
description:

pubspec.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ dependencies:
7676
sdk: flutter
7777
flutter_local_notifications: ^17.0.0
7878
dynamic_color: ^1.7.0
79+
webview_flutter: ^4.7.0
80+
webview_cookie_manager: ^2.0.6
7981

8082
dev_dependencies:
8183
flutter_test:

0 commit comments

Comments
 (0)