feat: OIDC Login on same page
This commit is contained in:
parent
8998d5600a
commit
df847abbeb
5 changed files with 252 additions and 78 deletions
|
|
@ -17,7 +17,7 @@ import 'package:fluffychat/pages/chat_members/chat_members.dart';
|
|||
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings.dart';
|
||||
import 'package:fluffychat/pages/chat_search/chat_search_page.dart';
|
||||
import 'package:fluffychat/pages/device_settings/device_settings.dart';
|
||||
import 'package:fluffychat/pages/intro/intro_page.dart';
|
||||
import 'package:fluffychat/pages/intro/intro_page_presenter.dart';
|
||||
import 'package:fluffychat/pages/invitation_selection/invitation_selection.dart';
|
||||
import 'package:fluffychat/pages/login/login.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group.dart';
|
||||
|
|
@ -68,7 +68,7 @@ abstract class AppRoutes {
|
|||
GoRoute(
|
||||
path: '/home',
|
||||
pageBuilder: (context, state) =>
|
||||
defaultPageBuilder(context, state, const IntroPage()),
|
||||
defaultPageBuilder(context, state, const IntroPagePresenter()),
|
||||
redirect: loggedInRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
|
|
@ -263,8 +263,11 @@ abstract class AppRoutes {
|
|||
GoRoute(
|
||||
path: 'addaccount',
|
||||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) =>
|
||||
defaultPageBuilder(context, state, const IntroPage()),
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const IntroPagePresenter(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'sign_in',
|
||||
|
|
|
|||
|
|
@ -13,7 +13,14 @@ import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class IntroPage extends StatelessWidget {
|
||||
const IntroPage({super.key});
|
||||
final bool isLoading;
|
||||
final String? loggingInToHomeserver;
|
||||
|
||||
const IntroPage({
|
||||
required this.isLoading,
|
||||
required this.loggingInToHomeserver,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -21,6 +28,7 @@ class IntroPage extends StatelessWidget {
|
|||
final addMultiAccount = Matrix.of(
|
||||
context,
|
||||
).widget.clients.any((client) => client.isLogged());
|
||||
final loggingInToHomeserver = this.loggingInToHomeserver;
|
||||
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
|
|
@ -35,7 +43,7 @@ class IntroPage extends StatelessWidget {
|
|||
useRootNavigator: true,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => restoreBackupFlow(context),
|
||||
onTap: isLoading ? null : () => restoreBackupFlow(context),
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
|
|
@ -71,17 +79,32 @@ class IntroPage extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
body: isLoading
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
CircularProgressIndicator.adaptive(),
|
||||
if (loggingInToHomeserver != null)
|
||||
Text(L10n.of(context).logInTo(loggingInToHomeserver)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Hero(
|
||||
tag: 'info-logo',
|
||||
child: Image.asset(
|
||||
|
|
@ -92,7 +115,9 @@ class IntroPage extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32.0,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: L10n.of(context).appIntro,
|
||||
textScaleFactor: MediaQuery.textScalerOf(
|
||||
|
|
@ -115,13 +140,17 @@ class IntroPage extends StatelessWidget {
|
|||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.secondary,
|
||||
foregroundColor: theme.colorScheme.onSecondary,
|
||||
backgroundColor:
|
||||
theme.colorScheme.secondary,
|
||||
foregroundColor:
|
||||
theme.colorScheme.onSecondary,
|
||||
),
|
||||
onPressed: () => context.go(
|
||||
'${GoRouterState.of(context).uri.path}/sign_up',
|
||||
),
|
||||
child: Text(L10n.of(context).createNewAccount),
|
||||
child: Text(
|
||||
L10n.of(context).createNewAccount,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
|
|
@ -140,7 +169,9 @@ class IntroPage extends StatelessWidget {
|
|||
extra: client,
|
||||
);
|
||||
},
|
||||
child: Text(L10n.of(context).loginWithMatrixId),
|
||||
child: Text(
|
||||
L10n.of(context).loginWithMatrixId,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
97
lib/pages/intro/intro_page_presenter.dart
Normal file
97
lib/pages/intro/intro_page_presenter.dart
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:matrix/matrix_api_lite/utils/logs.dart';
|
||||
import 'package:matrix/msc_extensions/msc_2964_oidc_login_flow/msc_2964_oidc_login_flow.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/universal_html.dart' as web;
|
||||
|
||||
import 'package:fluffychat/pages/intro/intro_page.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/oidc_session_json_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class IntroPagePresenter extends StatefulWidget {
|
||||
const IntroPagePresenter({super.key});
|
||||
|
||||
@override
|
||||
State<IntroPagePresenter> createState() => _IntroPagePresenterState();
|
||||
}
|
||||
|
||||
class _IntroPagePresenterState extends State<IntroPagePresenter> {
|
||||
bool isLoading = kIsWeb;
|
||||
String? loggingInToHomeserver;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (kIsWeb) _finishOidcLogin();
|
||||
}
|
||||
|
||||
void _finishOidcLogin() async {
|
||||
final store = await SharedPreferences.getInstance();
|
||||
final storedHomeserverString = store.getString(
|
||||
OidcSessionJsonExtension.homeserverStoreKey,
|
||||
);
|
||||
final homeserverUrl = storedHomeserverString == null
|
||||
? null
|
||||
: Uri.tryParse(storedHomeserverString);
|
||||
|
||||
final oidcSessionString = store.getString(
|
||||
OidcSessionJsonExtension.storeKey,
|
||||
);
|
||||
final session = oidcSessionString == null
|
||||
? null
|
||||
: OidcSessionJsonExtension.fromJson(jsonDecode(oidcSessionString));
|
||||
|
||||
await store.remove(OidcSessionJsonExtension.storeKey);
|
||||
await store.remove(OidcSessionJsonExtension.homeserverStoreKey);
|
||||
|
||||
if (homeserverUrl == null || session == null) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
loggingInToHomeserver = homeserverUrl.origin;
|
||||
});
|
||||
|
||||
try {
|
||||
final returnUrl = Uri.parse(web.window.location.href);
|
||||
final queryParameters = returnUrl.hasFragment
|
||||
? Uri.parse(returnUrl.fragment).queryParameters
|
||||
: returnUrl.queryParameters;
|
||||
final code = queryParameters['code'] as String;
|
||||
final state = queryParameters['state'] as String;
|
||||
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
await client.checkHomeserver(homeserverUrl);
|
||||
await client.oidcLogin(session: session, code: code, state: state);
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to login via OIDC', e, s);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IntroPage(
|
||||
isLoading: isLoading,
|
||||
loggingInToHomeserver: loggingInToHomeserver,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/oidc_session_json_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
Future<void> oidcLoginFlow(
|
||||
|
|
@ -16,9 +20,7 @@ Future<void> oidcLoginFlow(
|
|||
) async {
|
||||
Logs().i('Starting Matrix Native OIDC Flow...');
|
||||
final redirectUrl = kIsWeb
|
||||
? Uri.parse(
|
||||
html.window.location.href,
|
||||
).resolveUri(Uri(pathSegments: ['auth.html']))
|
||||
? Uri.parse(html.window.location.href).resolveUri(Uri(query: ''))
|
||||
: (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS)
|
||||
? Uri.parse('${AppConfig.appOpenUrlScheme.toLowerCase()}:/login')
|
||||
: Uri.parse('http://localhost:3001/login');
|
||||
|
|
@ -62,11 +64,28 @@ Future<void> oidcLoginFlow(
|
|||
|
||||
if (!context.mounted) return;
|
||||
|
||||
if (kIsWeb) {
|
||||
final store = await SharedPreferences.getInstance();
|
||||
store.setString(
|
||||
OidcSessionJsonExtension.homeserverStoreKey,
|
||||
client.homeserver!.toString(),
|
||||
);
|
||||
store.setString(
|
||||
OidcSessionJsonExtension.storeKey,
|
||||
jsonEncode(session.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
final returnUrlString = await FlutterWebAuth2.authenticate(
|
||||
url: session.authenticationUri.toString(),
|
||||
callbackUrlScheme: urlScheme,
|
||||
options: FlutterWebAuth2Options(useWebview: PlatformInfos.isMobile),
|
||||
options: FlutterWebAuth2Options(
|
||||
useWebview: PlatformInfos.isMobile,
|
||||
windowName: '_self',
|
||||
),
|
||||
);
|
||||
if (kIsWeb) return; // On Web we return at intro page when app starts again!
|
||||
|
||||
final returnUrl = Uri.parse(returnUrlString);
|
||||
final queryParameters = returnUrl.hasFragment
|
||||
? Uri.parse(returnUrl.fragment).queryParameters
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
extension OidcSessionJsonExtension on OidcLoginSession {
|
||||
static const String storeKey = 'oidc_session';
|
||||
static const String homeserverStoreKey = 'oidc_stored_homeserver';
|
||||
Map<String, Object?> toJson() => {
|
||||
'oidc_client_data': oidcClientData.toJson(),
|
||||
'authentication_uri': authenticationUri.toString(),
|
||||
'redirect_uri': redirectUri.toString(),
|
||||
'code_verifier': codeVerifier,
|
||||
'state': state,
|
||||
};
|
||||
|
||||
static OidcLoginSession fromJson(Map<String, Object?> json) =>
|
||||
OidcLoginSession(
|
||||
oidcClientData: OidcClientData.fromJson(
|
||||
json['oidc_client_data'] as Map<String, Object?>,
|
||||
),
|
||||
authenticationUri: Uri.parse(json['authentication_uri'] as String),
|
||||
redirectUri: Uri.parse(json['redirect_uri'] as String),
|
||||
codeVerifier: json['code_verifier'] as String,
|
||||
state: json['state'] as String,
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue