Skip to content

Commit 21f4330

Browse files
TF-3455 Add applicative token login method
1 parent be36476 commit 21f4330

File tree

4 files changed

+176
-12
lines changed

4 files changed

+176
-12
lines changed

lib/features/login/presentation/login_controller.dart

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'package:model/account/password.dart';
1717
import 'package:model/oidc/oidc_configuration.dart';
1818
import 'package:model/oidc/request/oidc_request.dart';
1919
import 'package:model/oidc/response/oidc_response.dart';
20+
import 'package:model/oidc/token_id.dart';
2021
import 'package:model/oidc/token_oidc.dart';
2122
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
2223
import 'package:tmail_ui_user/features/home/domain/state/auto_sign_in_via_deep_link_state.dart';
@@ -96,6 +97,7 @@ class LoginController extends ReloadableController {
9697
UserName? _username;
9798
Password? _password;
9899
Uri? _baseUri;
100+
String _applicativeToken = '';
99101

100102
DeepLinksManager? _deepLinksManager;
101103
StreamSubscription<DeepLinkData?>? _deepLinkDataStreamSubscription;
@@ -368,7 +370,7 @@ class LoginController extends ReloadableController {
368370
consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundBaseUrl()))));
369371
} else if (_username == null) {
370372
consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundUserName()))));
371-
} else if (_password == null) {
373+
} else if (_password == null && _applicativeToken.isEmpty) {
372374
consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundPassword()))));
373375
} else {
374376
if (PlatformInfo.isMobile && loginFormType.value == LoginFormType.credentialForm) {
@@ -378,13 +380,17 @@ class LoginController extends ReloadableController {
378380
}
379381
}
380382

381-
consumeState(
382-
_authenticationInteractor.execute(
383-
baseUrl: _currentBaseUrl!,
384-
userName: _username!,
385-
password: _password!
386-
)
387-
);
383+
if (_password != null) {
384+
consumeState(
385+
_authenticationInteractor.execute(
386+
baseUrl: _currentBaseUrl!,
387+
userName: _username!,
388+
password: _password!
389+
)
390+
);
391+
} else {
392+
_loginByApplicativeToken(_applicativeToken);
393+
}
388394
}
389395
}
390396

@@ -621,6 +627,23 @@ class LoginController extends ReloadableController {
621627
loginFormType.value == LoginFormType.passwordForm ||
622628
loginFormType.value == LoginFormType.credentialForm;
623629

630+
void onApplicativeTokenChange(String value) {
631+
_applicativeToken = value;
632+
}
633+
634+
void _loginByApplicativeToken(String token) {
635+
_synchronizeTokenAndGetSession(
636+
baseUri: _currentBaseUrl!,
637+
tokenOIDC: TokenOIDC(_applicativeToken, TokenId(uuid.v4()), ''),
638+
oidcConfiguration: OIDCConfiguration(
639+
authority: '',
640+
clientId: '',
641+
scopes: [],
642+
),
643+
);
644+
getSessionAction();
645+
}
646+
624647
@override
625648
void onClose() {
626649
passFocusNode.dispose();

lib/features/login/presentation/login_view.dart

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import 'package:core/presentation/state/success.dart';
33
import 'package:core/presentation/utils/theme_utils.dart';
44
import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart';
55
import 'package:flutter/material.dart';
6+
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
67
import 'package:get/get.dart';
78
import 'package:tmail_ui_user/features/base/widget/application_version_widget.dart';
89
import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart';
910
import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart';
1011
import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart';
1112
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
1213
import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart';
14+
import 'package:tmail_ui_user/features/login/presentation/widgets/applicative_token_field.dart';
1315
import 'package:tmail_ui_user/features/login/presentation/widgets/dns_lookup_input_form.dart';
1416
import 'package:tmail_ui_user/features/login/presentation/widgets/horizontal_progress_loading_button.dart';
1517
import 'package:tmail_ui_user/features/login/presentation/widgets/login_back_button.dart';
@@ -122,12 +124,34 @@ class LoginView extends BaseLoginView {
122124
return const SizedBox.shrink();
123125
}
124126
}),
127+
Obx(() {
128+
switch (controller.loginFormType.value) {
129+
case LoginFormType.passwordForm:
130+
return ApplicativeTokenField(
131+
onChanged: controller.onApplicativeTokenChange,
132+
appLocalizations: AppLocalizations.of(context),
133+
imagePath: controller.imagePaths,
134+
);
135+
default:
136+
return const SizedBox.shrink();
137+
}
138+
}),
125139
_buildLoadingProgress(context),
126-
const Padding(
127-
padding: EdgeInsets.only(top: 16),
128-
child: PrivacyLinkWidget(),
140+
KeyboardVisibilityBuilder(
141+
builder: (context, visible) {
142+
if (visible) return const SizedBox.shrink();
143+
144+
return const Column(
145+
children: [
146+
Padding(
147+
padding: EdgeInsets.only(top: 16),
148+
child: PrivacyLinkWidget(),
149+
),
150+
ApplicationVersionWidget(),
151+
],
152+
);
153+
},
129154
),
130-
const ApplicationVersionWidget(),
131155
]
132156
),
133157
)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:core/presentation/resources/image_paths.dart';
2+
import 'package:core/presentation/views/text/text_field_builder.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_svg/flutter_svg.dart';
5+
import 'package:tmail_ui_user/features/base/widget/text_input_decoration_builder.dart';
6+
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
7+
8+
class ApplicativeTokenField extends StatefulWidget {
9+
final ValueChanged<String> onChanged;
10+
final AppLocalizations appLocalizations;
11+
final ImagePaths? imagePath;
12+
13+
const ApplicativeTokenField({
14+
Key? key,
15+
required this.onChanged,
16+
required this.appLocalizations,
17+
this.imagePath,
18+
}) : super(key: key);
19+
20+
@override
21+
State<ApplicativeTokenField> createState() => _ApplicativeTokenFieldState();
22+
}
23+
24+
class _ApplicativeTokenFieldState extends State<ApplicativeTokenField> with SingleTickerProviderStateMixin {
25+
final controller = TextEditingController();
26+
final focusNode = FocusNode();
27+
late final AnimationController rotationController;
28+
29+
@override
30+
void initState() {
31+
super.initState();
32+
rotationController = AnimationController(
33+
duration: const Duration(milliseconds: 200),
34+
vsync: this,
35+
);
36+
}
37+
38+
@override
39+
void dispose() {
40+
controller.dispose();
41+
focusNode.dispose();
42+
rotationController.dispose();
43+
super.dispose();
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return ExpansionTile(
49+
title: Text(widget.appLocalizations.advancedSettings),
50+
controlAffinity: ListTileControlAffinity.leading,
51+
leading: widget.imagePath == null
52+
? null
53+
: RotationTransition(
54+
turns: Tween(begin: 0.0, end: 0.5).animate(rotationController),
55+
child: SvgPicture.asset(
56+
widget.imagePath!.icArrowDown,
57+
width: 24,
58+
height: 24,
59+
fit: BoxFit.fill,
60+
),
61+
),
62+
shape: const Border(),
63+
tilePadding: const EdgeInsets.symmetric(horizontal: 32),
64+
childrenPadding: const EdgeInsets.symmetric(horizontal: 32),
65+
onExpansionChanged: (isExpanded) {
66+
if (isExpanded) {
67+
rotationController.forward();
68+
} else {
69+
rotationController.reverse();
70+
}
71+
},
72+
children: [
73+
Row(
74+
children: [
75+
Text(
76+
widget.appLocalizations.applicativeToken,
77+
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
78+
),
79+
const SizedBox(width: 8),
80+
Expanded(
81+
child: TextFieldBuilder(
82+
controller: controller,
83+
focusNode: focusNode,
84+
autoFocus: true,
85+
onTextChange: widget.onChanged,
86+
maxLines: 1,
87+
decoration: TextInputDecorationBuilder().build(),
88+
),
89+
),
90+
],
91+
),
92+
Text(widget.appLocalizations.someJMAPServicesDoNotSupportLoginViaPassword),
93+
],
94+
);
95+
}
96+
}

lib/main/localizations/app_localizations.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4472,6 +4472,27 @@ class AppLocalizations {
44724472
);
44734473
}
44744474

4475+
String get advancedSettings {
4476+
return Intl.message(
4477+
'Advanced settings',
4478+
name: 'advancedSettings',
4479+
);
4480+
}
4481+
4482+
String get applicativeToken {
4483+
return Intl.message(
4484+
'Applicative token',
4485+
name: 'applicativeToken',
4486+
);
4487+
}
4488+
4489+
String get someJMAPServicesDoNotSupportLoginViaPassword {
4490+
return Intl.message(
4491+
'Some JMAP services do not support login via password for third party apps but instead allow generate applicative tokens.',
4492+
name: 'someJMAPServicesDoNotSupportLoginViaPassword',
4493+
);
4494+
}
4495+
44754496
String get downloadAttachmentInEMLPreviewWarningMessage {
44764497
return Intl.message(
44774498
'Downloading attachment. You can only download one file at a time.',

0 commit comments

Comments
 (0)