diff --git a/CHANGELOG.md b/CHANGELOG.md
index 989fe0a498..3f9c1e0f88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+## [0.14.0] - 2024-11-09
+### Added
+- #2387 TWP Registration from mobile
+- #3157 Web socket to replace FCM in web app
+- #2953 Mobile integration test with Patrol
+
+### Fixed
+- #3244 `To` filter should apply also for `cc` `bcc`
+- #3243 some tags still be in sanitizing
+- #3178 Only Space in Name verification for Identity, Rule Filter, Vacation
+- 3D links not work on mobile
+- Focus problem in `tab` in Basic Auth login form
+- #3225 Print button blink blink
+- #3222 Hide reply calendar event action button
+- #3247 Cc is lost if open email from Quick search result
+- #3200 Update option menu for personal folders
+- Support German
+
+## [0.13.6] - 2024-11-07
+### Fixed
+- Remove app grid in mobile
+
## [0.13.5] - 2024-10-24
### Fixed
- Sanitize html
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 07027d3fc6..e6ef49d68a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -79,12 +79,6 @@
-
-
-
-
-
-
@@ -98,6 +92,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/backend-docker/docker-compose.yaml b/backend-docker/docker-compose.yaml
index 2ce61d4f9d..36f851616e 100644
--- a/backend-docker/docker-compose.yaml
+++ b/backend-docker/docker-compose.yaml
@@ -10,6 +10,8 @@ services:
- ./mailetcontainer.xml:/root/conf/mailetcontainer.xml
- ./imapserver.xml:/root/conf/imapserver.xml
- ./jmap.properties:/root/conf/jmap.properties
+ - ../provisioning/integration_test/search_email_with_sort_order/provisioning.sh:/root/conf/integration_test/search_email_with_sort_order/provisioning.sh
+ - ../provisioning/integration_test/search_email_with_sort_order/eml:/root/conf/integration_test/search_email_with_sort_order/eml
ports:
- "80:80"
environment:
diff --git a/core/lib/core.dart b/core/lib/core.dart
index f887e6f075..bfe0664e63 100644
--- a/core/lib/core.dart
+++ b/core/lib/core.dart
@@ -8,6 +8,7 @@ export 'presentation/extensions/capitalize_extension.dart';
export 'presentation/extensions/list_extensions.dart';
export 'presentation/extensions/list_nullable_extensions.dart';
export 'domain/extensions/datetime_extension.dart';
+export 'domain/extensions/list_datetime_extension.dart';
export 'presentation/extensions/html_extension.dart';
export 'presentation/extensions/compare_string_extension.dart';
export 'presentation/extensions/compare_list_extensions.dart';
diff --git a/core/lib/data/constants/constant.dart b/core/lib/data/constants/constant.dart
index 5540b0ac1c..06b5d45eae 100644
--- a/core/lib/data/constants/constant.dart
+++ b/core/lib/data/constants/constant.dart
@@ -7,4 +7,6 @@ class Constant {
static const octetStreamMimeType = 'application/octet-stream';
static const pdfExtension = '.pdf';
static const imageType = 'image';
+ static const textVCardMimeType = 'text/x-vcard';
+ static const textPlainMimeType = 'text/plain';
}
\ No newline at end of file
diff --git a/core/lib/domain/extensions/list_datetime_extension.dart b/core/lib/domain/extensions/list_datetime_extension.dart
new file mode 100644
index 0000000000..a5af81996b
--- /dev/null
+++ b/core/lib/domain/extensions/list_datetime_extension.dart
@@ -0,0 +1,21 @@
+
+extension ListDateTimeExtension on List {
+
+ bool isSortedByMostRecent() {
+ for (int i = 0; i < length - 1; i++) {
+ if (this[i].isBefore(this[i + 1])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool isSortedByOldestFirst() {
+ for (int i = 0; i < length - 1; i++) {
+ if (this[i].isAfter(this[i + 1])) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart
index ce2516630c..52b6013b14 100644
--- a/core/lib/presentation/resources/image_paths.dart
+++ b/core/lib/presentation/resources/image_paths.dart
@@ -220,6 +220,7 @@ class ImagePaths {
String get icGoodSignature => _getImagePath('ic_good_signature.svg');
String get icBadSignature => _getImagePath('ic_bad_signature.svg');
String get icDeleteSelection => _getImagePath('ic_delete_selection.svg');
+ String get icLogoTwakeWelcome => _getImagePath('ic_logo_twake_welcome.svg');
String _getImagePath(String imageName) {
return AssetsPaths.images + imageName;
diff --git a/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
index 474b14dac5..e9e5b8c76b 100644
--- a/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
+++ b/core/lib/presentation/utils/html_transformer/text/standardize_html_sanitizing_transformers.dart
@@ -19,6 +19,7 @@ class StandardizeHtmlSanitizingTransformers extends TextTransformer {
'center',
'style',
'body',
+ 'section',
];
const StandardizeHtmlSanitizingTransformers();
diff --git a/core/lib/presentation/views/bottom_popup/cupertino_action_sheet_builder.dart b/core/lib/presentation/views/bottom_popup/cupertino_action_sheet_builder.dart
index 3f8ff78456..16cb45a939 100644
--- a/core/lib/presentation/views/bottom_popup/cupertino_action_sheet_builder.dart
+++ b/core/lib/presentation/views/bottom_popup/cupertino_action_sheet_builder.dart
@@ -6,13 +6,14 @@ import 'package:pointer_interceptor/pointer_interceptor.dart';
class CupertinoActionSheetBuilder {
final BuildContext _context;
+ final Key? key;
final List _actionTiles = [];
Widget? _titleWidget;
Widget? _messageWidget;
Widget? _cancelWidget;
- CupertinoActionSheetBuilder(this._context);
+ CupertinoActionSheetBuilder(this._context, {this.key});
void title(Widget? titleWidget) {
_titleWidget = titleWidget;
@@ -35,6 +36,7 @@ class CupertinoActionSheetBuilder {
context: _context,
barrierColor: AppColor.colorDefaultCupertinoActionSheet,
builder: (context) => PointerInterceptor(child: CupertinoActionSheet(
+ key: key,
title: _titleWidget,
message: _messageWidget,
actions: _actionTiles,
diff --git a/core/test/utils/standardize_html_sanitizing_transformers_test.dart b/core/test/utils/standardize_html_sanitizing_transformers_test.dart
index 2249cac95e..5910914dc4 100644
--- a/core/test/utils/standardize_html_sanitizing_transformers_test.dart
+++ b/core/test/utils/standardize_html_sanitizing_transformers_test.dart
@@ -89,7 +89,7 @@ void main() {
];
const listHTMLTags = [
- 'div', 'span', 'p', 'a', 'u', 'i', 'table'
+ 'div', 'span', 'p', 'a', 'u', 'i', 'table', 'section'
];
for (var tag in listHTMLTags) {
diff --git a/docs/adr/0036-mailto-uri-chemes-to-interact-twake-mail.md b/docs/adr/0036-mailto-uri-chemes-to-interact-twake-mail.md
index 1f75e80813..2faa805420 100644
--- a/docs/adr/0036-mailto-uri-chemes-to-interact-twake-mail.md
+++ b/docs/adr/0036-mailto-uri-chemes-to-interact-twake-mail.md
@@ -18,13 +18,13 @@ Summary of URI schemes that can interact with Twake Mail:
- `/mailto?uri=user@example.com`
- `/mailto/?uri=mailto:user@example.com&subject=TwakeMail&body=HelloWorld`
- - `/mailto/?uri=mailto:user1@example.com,user2@example.com,user3@example.com&subject=TwakeMail&body=HelloWorld`
+ - `/mailto/?uri=mailto:user1@example.com,user2@example.com,user3@example.com&to=user1@example.com,user2@example.com,user3@example.com&cc=user1@example.com,user2@example.com,user3@example.com&bcc=user1@example.com,user2@example.com,user3@example.com&subject=TwakeMail&body=HelloWorld`
2. URI scheme encoded
- `%2Fmailto%3Furi%3Duser%40example.com`
- `%2Fmailto%2F%3Furi%3Dmailto%3Auser%40example.com%26subject%3DTwakeMail%26body%3DHelloWorld`
- - `%2Fmailto%2F%3Furi%3Dmailto%3Auser1%40example.com%2Cuser2%40example.com%2Cuser3%40example.com%26subject%3DTwakeMail%26body%3DHelloWorld`
+ - `%2Fmailto%2F%3Furi%3Dmailto%3Auser1%40example.com%2Cuser2%40example.com%2Cuser3%40example.com%26to=user1%40example.com%2Cuser2%40example.com%2Cuser3%40example.com%26cc=user1%40example.com%2Cuser2%40example.com%2Cuser3%40example.com%26bcc=user1%40example.com%2Cuser2%40example.com%2Cuser3%40example.com%26subject%3DTwakeMail%26body%3DHelloWorld`
## Consequences
diff --git a/integration_test/base/base_scenario.dart b/integration_test/base/base_scenario.dart
index a9445902e2..1bd8e923f5 100644
--- a/integration_test/base/base_scenario.dart
+++ b/integration_test/base/base_scenario.dart
@@ -1,3 +1,4 @@
+import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
abstract class BaseScenario {
@@ -6,4 +7,9 @@ abstract class BaseScenario {
const BaseScenario(this.$);
Future execute();
+
+ Future expectViewVisible(PatrolFinder patrolFinder) async {
+ await $.waitUntilVisible(patrolFinder);
+ expect(patrolFinder, findsWidgets);
+ }
}
\ No newline at end of file
diff --git a/integration_test/base/core_robot.dart b/integration_test/base/core_robot.dart
index a116b25539..7d586037b8 100644
--- a/integration_test/base/core_robot.dart
+++ b/integration_test/base/core_robot.dart
@@ -1,4 +1,3 @@
-import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
abstract class CoreRobot {
@@ -6,10 +5,5 @@ abstract class CoreRobot {
CoreRobot(this.$);
- Future ensureViewVisible(PatrolFinder patrolFinder) async {
- await $.waitUntilVisible(patrolFinder);
- expect(patrolFinder, findsWidgets);
- }
-
dynamic ignoreException() => $.tester.takeException();
}
\ No newline at end of file
diff --git a/integration_test/robots/composer_robot.dart b/integration_test/robots/composer_robot.dart
index e8cd076a79..02786f2fc7 100644
--- a/integration_test/robots/composer_robot.dart
+++ b/integration_test/robots/composer_robot.dart
@@ -50,10 +50,6 @@ class ComposerRobot extends CoreRobot {
.tap();
}
- Future expectSendEmailSuccessToast() async {
- expect($('Message has been sent successfully'), findsOneWidget);
- }
-
Future grantContactPermission() async {
if (await $.native.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
await $.native.grantPermissionWhenInUse();
diff --git a/integration_test/robots/login_robot.dart b/integration_test/robots/login_robot.dart
index b079396fea..25edc9aef0 100644
--- a/integration_test/robots/login_robot.dart
+++ b/integration_test/robots/login_robot.dart
@@ -1,15 +1,26 @@
import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart';
import 'package:flutter/material.dart';
+import 'package:patrol/patrol.dart';
import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart';
import 'package:tmail_ui_user/features/login/presentation/login_view.dart';
import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart';
+import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import '../base/core_robot.dart';
class LoginRobot extends CoreRobot {
LoginRobot(super.$);
- Future expectLoginViewVisible() => ensureViewVisible($(LoginView));
+ Future grantNotificationPermission(NativeAutomator nativeAutomator) async {
+ if (await nativeAutomator.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
+ await nativeAutomator.grantPermissionWhenInUse();
+ }
+ }
+
+ Future tapOnUseCompanyServer() async {
+ await $.pumpAndSettle();
+ await $(AppLocalizations().useCompanyServer).tap();
+ }
Future enterEmail(String email) async {
final finder = $(LoginView).$(TextField);
diff --git a/integration_test/robots/search_robot.dart b/integration_test/robots/search_robot.dart
new file mode 100644
index 0000000000..8923b5091b
--- /dev/null
+++ b/integration_test/robots/search_robot.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../base/core_robot.dart';
+
+class SearchRobot extends CoreRobot {
+ SearchRobot(super.$);
+
+ Future enterQueryString(String queryString) async {
+ await $(#search_email_text_field).enterText(queryString);
+ }
+
+ Future scrollToEndListSearchFilter() async {
+ await $.scrollUntilVisible(
+ finder: $(#mobile_sortBy_search_filter_button),
+ view: $(#search_filter_list_view),
+ scrollDirection: AxisDirection.right,
+ delta: 300,
+ );
+ }
+
+ Future openSortOrderBottomDialog() async {
+ await $(#mobile_sortBy_search_filter_button).tap();
+ }
+
+ Future selectSortOrder(String sortOrderName) async {
+ await $(find.text(sortOrderName)).tap();
+ await $.pump(const Duration(seconds: 2));
+ }
+}
\ No newline at end of file
diff --git a/integration_test/robots/thread_robot.dart b/integration_test/robots/thread_robot.dart
index d2dd8ee3c4..6ff0767b1e 100644
--- a/integration_test/robots/thread_robot.dart
+++ b/integration_test/robots/thread_robot.dart
@@ -1,18 +1,17 @@
+import 'package:core/presentation/views/search/search_bar_view.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart';
-import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
-import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
import '../base/core_robot.dart';
class ThreadRobot extends CoreRobot {
ThreadRobot(super.$);
- Future expectThreadViewVisible() => ensureViewVisible($(ThreadView));
-
Future openComposer() async {
await $(ComposeFloatingButton).$(InkWell).tap();
}
- Future expectComposerViewVisible() => ensureViewVisible($(ComposerView));
+ Future openSearchView() async {
+ await $(SearchBarView).$(InkWell).tap();
+ }
}
\ No newline at end of file
diff --git a/integration_test/scenarios/login_with_basic_auth_scenario.dart b/integration_test/scenarios/login_with_basic_auth_scenario.dart
index f83ebd081d..a11320d881 100644
--- a/integration_test/scenarios/login_with_basic_auth_scenario.dart
+++ b/integration_test/scenarios/login_with_basic_auth_scenario.dart
@@ -1,9 +1,11 @@
+import 'package:tmail_ui_user/features/login/presentation/login_view.dart';
+import 'package:tmail_ui_user/features/starting_page/presentation/twake_welcome/twake_welcome_view.dart';
+import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
+
import '../base/base_scenario.dart';
import '../robots/login_robot.dart';
-import '../robots/thread_robot.dart';
-import '../utils/scenario_utils_mixin.dart';
-class LoginWithBasicAuthScenario extends BaseScenario with ScenarioUtilsMixin {
+class LoginWithBasicAuthScenario extends BaseScenario {
const LoginWithBasicAuthScenario(
super.$,
{
@@ -22,9 +24,12 @@ class LoginWithBasicAuthScenario extends BaseScenario with ScenarioUtilsMixin {
@override
Future execute() async {
final loginRobot = LoginRobot($);
- final threadRobot = ThreadRobot($);
- await loginRobot.expectLoginViewVisible();
+ await _expectWelcomeViewVisible();
+
+ await loginRobot.tapOnUseCompanyServer();
+ await _expectLoginViewVisible();
+
await loginRobot.enterEmail(username);
await loginRobot.enterHostUrl(hostUrl);
@@ -32,8 +37,14 @@ class LoginWithBasicAuthScenario extends BaseScenario with ScenarioUtilsMixin {
await loginRobot.enterBasicAuthPassword(password);
await loginRobot.loginBasicAuth();
- await grantNotificationPermission($.native);
+ await loginRobot.grantNotificationPermission($.native);
- await threadRobot.expectThreadViewVisible();
+ await _expectThreadViewVisible();
}
+
+ Future _expectWelcomeViewVisible() => expectViewVisible($(TwakeWelcomeView));
+
+ Future _expectLoginViewVisible() => expectViewVisible($(LoginView));
+
+ Future _expectThreadViewVisible() => expectViewVisible($(ThreadView));
}
\ No newline at end of file
diff --git a/integration_test/scenarios/search_email_with_sort_order_scenario.dart b/integration_test/scenarios/search_email_with_sort_order_scenario.dart
new file mode 100644
index 0000000000..bac755a993
--- /dev/null
+++ b/integration_test/scenarios/search_email_with_sort_order_scenario.dart
@@ -0,0 +1,198 @@
+
+import 'package:collection/collection.dart';
+import 'package:core/domain/extensions/list_datetime_extension.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart';
+import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart';
+import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart';
+import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
+
+import '../base/base_scenario.dart';
+import '../robots/search_robot.dart';
+import '../robots/thread_robot.dart';
+import 'login_with_basic_auth_scenario.dart';
+
+class SearchEmailWithSortOrderScenario extends BaseScenario {
+ const SearchEmailWithSortOrderScenario(
+ super.$,
+ {
+ required this.loginWithBasicAuthScenario,
+ required this.queryString,
+ required this.listUsername,
+ }
+ );
+
+ final LoginWithBasicAuthScenario loginWithBasicAuthScenario;
+ final String queryString;
+ final List listUsername;
+
+ @override
+ Future execute() async {
+ await loginWithBasicAuthScenario.execute();
+
+ final threadRobot = ThreadRobot($);
+ await threadRobot.openSearchView();
+ await _expectSearchViewVisible();
+
+ final searchRobot = SearchRobot($);
+ await searchRobot.enterQueryString(queryString);
+ await _expectSuggestionSearchListViewVisible();
+
+ await searchRobot.scrollToEndListSearchFilter();
+ await _expectSortBySearchFilterButtonVisible();
+
+ final appLocalizations = AppLocalizations();
+
+ for (var sortOrderType in EmailSortOrderType.values) {
+ await _performSelectSortOrderAndValidateSearchResult(
+ searchRobot: searchRobot,
+ sortOrderType: sortOrderType,
+ appLocalizations: appLocalizations,
+ );
+ }
+ }
+
+ Future _performSelectSortOrderAndValidateSearchResult({
+ required SearchRobot searchRobot,
+ required EmailSortOrderType sortOrderType,
+ required AppLocalizations appLocalizations,
+ }) async {
+ await Future.delayed(const Duration(seconds: 2));
+
+ await searchRobot.openSortOrderBottomDialog();
+ await _expectSortFilterContextMenuVisible();
+
+ await searchRobot.selectSortOrder(sortOrderType.getTitleByAppLocalizations(appLocalizations));
+ await _expectSearchResultEmailListVisible();
+ await _expectEmailListDisplayedCorrectly(listUsername: listUsername);
+
+ switch (sortOrderType) {
+ case EmailSortOrderType.mostRecent:
+ await _expectEmailListSortedCorrectByMostRecent();
+ break;
+ case EmailSortOrderType.oldest:
+ await _expectEmailListSortedCorrectByOldest();
+ break;
+ case EmailSortOrderType.senderAscending:
+ await _expectEmailListSortedCorrectBySenderAscending(listUsername: listUsername);
+ break;
+ case EmailSortOrderType.senderDescending:
+ await _expectEmailListSortedCorrectBySenderDescending(listUsername: listUsername);
+ break;
+ case EmailSortOrderType.subjectAscending:
+ await _expectEmailListSortedCorrectBySubjectAscending(listUsername: listUsername);
+ break;
+ case EmailSortOrderType.subjectDescending:
+ await _expectEmailListSortedCorrectBySubjectDescending(listUsername: listUsername);
+ break;
+ case EmailSortOrderType.relevance:
+ break;
+ }
+ }
+
+ Future _expectSearchViewVisible() async {
+ await expectViewVisible($(SearchEmailView));
+ }
+
+ Future _expectSuggestionSearchListViewVisible() async {
+ await expectViewVisible($(#suggestion_search_list_view));
+ }
+
+ Future _expectSortBySearchFilterButtonVisible() async {
+ await expectViewVisible($(#mobile_sortBy_search_filter_button));
+ }
+
+ Future _expectSortFilterContextMenuVisible() async {
+ await expectViewVisible($(#sort_filter_context_menu));
+ }
+
+ Future _expectSearchResultEmailListVisible() async {
+ await expectViewVisible($(#search_email_list_notification_listener));
+ }
+
+ Future _expectEmailListDisplayedCorrectly({
+ required List listUsername,
+ }) async {
+ expect(find.byType(EmailTileBuilder), findsNWidgets(listUsername.length));
+ }
+
+ Future _expectEmailListSortedCorrectBySenderAscending({
+ required List listUsername,
+ }) async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ for (int i = 0; i < listUsername.length; i++) {
+ EmailTileBuilder emailTile = listEmailTile.elementAt(i);
+ final senderName = emailTile.presentationEmail.firstEmailAddressInFrom;
+ expect(senderName, equals('${listUsername[i].toLowerCase()}@example.com'));
+ }
+ }
+
+ Future _expectEmailListSortedCorrectBySenderDescending({
+ required List listUsername,
+ }) async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ final reversedListUsername = listUsername.reversed.toList();
+
+ for (int i = 0; i < reversedListUsername.length; i++) {
+ EmailTileBuilder emailTile = listEmailTile.elementAt(i);
+ final senderName = emailTile.presentationEmail.firstEmailAddressInFrom;
+ expect(senderName, equals('${reversedListUsername[i].toLowerCase()}@example.com'));
+ }
+ }
+
+ Future _expectEmailListSortedCorrectBySubjectAscending({
+ required List listUsername,
+ }) async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ for (int i = 0; i < listUsername.length; i++) {
+ EmailTileBuilder emailTile = listEmailTile.elementAt(i);
+ final subject = emailTile.presentationEmail.subject;
+ expect(subject, equals('${listUsername[i]} send Bob'));
+ }
+ }
+
+ Future _expectEmailListSortedCorrectBySubjectDescending({
+ required List listUsername,
+ }) async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ final reversedListUsername = listUsername.reversed.toList();
+
+ for (int i = 0; i < reversedListUsername.length; i++) {
+ EmailTileBuilder emailTile = listEmailTile.elementAt(i);
+ final subject = emailTile.presentationEmail.subject;
+ expect(subject, equals('${reversedListUsername[i]} send Bob'));
+ }
+ }
+
+ Future _expectEmailListSortedCorrectByMostRecent() async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ final listReceiveAtTime = listEmailTile
+ .map((emailTile) => emailTile.presentationEmail.receivedAt?.value)
+ .whereNotNull()
+ .toList();
+
+ expect(listReceiveAtTime.isSortedByMostRecent(), isTrue);
+ }
+
+ Future _expectEmailListSortedCorrectByOldest() async {
+ final listEmailTile = $.tester
+ .widgetList(find.byType(EmailTileBuilder));
+
+ final listReceiveAtTime = listEmailTile
+ .map((emailTile) => emailTile.presentationEmail.receivedAt?.value)
+ .whereNotNull()
+ .toList();
+
+ expect(listReceiveAtTime.isSortedByOldestFirst(), isTrue);
+ }
+}
\ No newline at end of file
diff --git a/integration_test/scenarios/send_email_scenario.dart b/integration_test/scenarios/send_email_scenario.dart
index 8d9184fa85..e0d5f6b8a0 100644
--- a/integration_test/scenarios/send_email_scenario.dart
+++ b/integration_test/scenarios/send_email_scenario.dart
@@ -1,3 +1,6 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
+
import '../base/base_scenario.dart';
import '../robots/composer_robot.dart';
import '../robots/thread_robot.dart';
@@ -27,7 +30,7 @@ class SendEmailScenario extends BaseScenario {
await loginWithBasicAuthScenario.execute();
await threadRobot.openComposer();
- await threadRobot.expectComposerViewVisible();
+ await _expectComposerViewVisible();
await composerRobot.grantContactPermission();
@@ -36,6 +39,13 @@ class SendEmailScenario extends BaseScenario {
await composerRobot.addSubject(subject);
await composerRobot.addContent(content);
await composerRobot.sendEmail();
- await composerRobot.expectSendEmailSuccessToast();
+
+ await _expectSendEmailSuccessToast();
+ }
+
+ Future _expectComposerViewVisible() => expectViewVisible($(ComposerView));
+
+ Future _expectSendEmailSuccessToast() async {
+ expect($('Message has been sent successfully'), findsOneWidget);
}
}
\ No newline at end of file
diff --git a/integration_test/tests/search/search_email_with_sort_order_test.dart b/integration_test/tests/search/search_email_with_sort_order_test.dart
new file mode 100644
index 0000000000..1f6001e87b
--- /dev/null
+++ b/integration_test/tests/search/search_email_with_sort_order_test.dart
@@ -0,0 +1,35 @@
+import '../../base/test_base.dart';
+import '../../scenarios/login_with_basic_auth_scenario.dart';
+import '../../scenarios/search_email_with_sort_order_scenario.dart';
+
+void main() {
+ TestBase().runPatrolTest(
+ description: 'Should see list email displayed by sort order selected when search email successfully',
+ test: ($) async {
+ const username = String.fromEnvironment('USERNAME');
+ const password = String.fromEnvironment('PASSWORD');
+ const hostUrl = String.fromEnvironment('BASIC_AUTH_URL');
+ const email = String.fromEnvironment('BASIC_AUTH_EMAIL');
+
+ final loginWithBasicAuthScenario = LoginWithBasicAuthScenario(
+ $,
+ username: username,
+ password: password,
+ hostUrl: hostUrl,
+ email: email,
+ );
+
+ const queryString = 'hello';
+ const listUsername = ['Alice', 'Brian', 'Charlotte', 'David', 'Emma'];
+
+ final searchEmailWithSortOrderScenario = SearchEmailWithSortOrderScenario(
+ $,
+ loginWithBasicAuthScenario: loginWithBasicAuthScenario,
+ queryString: queryString,
+ listUsername: listUsername
+ );
+
+ await searchEmailWithSortOrderScenario.execute();
+ },
+ );
+}
\ No newline at end of file
diff --git a/integration_test/utils/scenario_utils_mixin.dart b/integration_test/utils/scenario_utils_mixin.dart
deleted file mode 100644
index 2b119c5101..0000000000
--- a/integration_test/utils/scenario_utils_mixin.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-import 'package:patrol/patrol.dart';
-
-mixin ScenarioUtilsMixin {
- Future grantNotificationPermission(NativeAutomator nativeAutomator) async {
- if (await nativeAutomator.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
- await nativeAutomator.grantPermissionWhenInUse();
- }
- }
-}
\ No newline at end of file
diff --git a/ios/Podfile b/ios/Podfile
index d1c2687646..8148d12783 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -32,6 +32,10 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+
+ target 'TeamMailShareExtension' do
+ inherit! :search_paths
+ end
end
post_install do |installer|
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index d1d2b04611..cd8c1e4fa6 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -9,6 +9,7 @@ PODS:
- AppAuth/Core
- better_open_file (0.0.1):
- Flutter
+ - CocoaAsyncSocket (7.6.5)
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
@@ -116,6 +117,8 @@ PODS:
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
+ - flutter_web_auth_2 (3.0.0):
+ - Flutter
- GoogleDataTransport (9.3.0):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
@@ -163,17 +166,24 @@ PODS:
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - patrol (0.0.1):
+ - CocoaAsyncSocket (~> 7.6)
+ - Flutter
+ - FlutterMacOS
- pdf_render (0.0.1):
- Flutter
- permission_handler_apple (9.1.1):
- Flutter
+ - photo_manager (2.0.0):
+ - Flutter
+ - FlutterMacOS
- pointer_interceptor_ios (0.0.1):
- Flutter
- printing (1.0.0):
- Flutter
- PromisesObjC (2.3.1)
- ReachabilitySwift (5.0.0)
- - receive_sharing_intent (0.0.1):
+ - receive_sharing_intent (1.8.1):
- Flutter
- SDWebImage (5.18.10):
- SDWebImage/Core (= 5.18.10)
@@ -214,10 +224,13 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
+ - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - patrol (from `.symlinks/plugins/patrol/darwin`)
- pdf_render (from `.symlinks/plugins/pdf_render/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+ - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- printing (from `.symlinks/plugins/printing/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
@@ -229,6 +242,7 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- AppAuth
+ - CocoaAsyncSocket
- DKImagePickerController
- DKPhotoGallery
- Firebase
@@ -290,14 +304,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
+ flutter_web_auth_2:
+ :path: ".symlinks/plugins/flutter_web_auth_2/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
+ patrol:
+ :path: ".symlinks/plugins/patrol/darwin"
pdf_render:
:path: ".symlinks/plugins/pdf_render/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
+ photo_manager:
+ :path: ".symlinks/plugins/photo_manager/ios"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
printing:
@@ -317,6 +337,7 @@ SPEC CHECKSUMS:
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
AppAuth: 182c5b88630569df5acb672720534756c29b3358
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
+ CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e
contacts_service: 849e1f84281804c8bfbec1b4c3eedcb23c5d3eca
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
@@ -342,6 +363,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
+ flutter_web_auth_2: 051cf9f5dc366f31b5dcc4e2952c2b954767be8a
GoogleDataTransport: 57c22343ab29bc686febbf7cbb13bad167c2d8fe
GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
@@ -350,13 +372,15 @@ SPEC CHECKSUMS:
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+ patrol: 0564cee315ff6c86fb802b3647db05cc2d3d0624
pdf_render: 0b4e1a615aab83ce88b26c57753049424908a755
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
+ photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
pointer_interceptor_ios: 9280618c0b2eeb80081a343924aa8ad756c21375
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
- receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
+ receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec
SDWebImageWebPCoder: 633b813fca24f1de5e076bcd7f720c038b23892b
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -366,6 +390,6 @@ SPEC CHECKSUMS:
url_launcher_ios: 68d46cc9766d0c41dbdc884310529557e3cd7a86
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
-PODFILE CHECKSUM: ded4724b53568389542fa0f7d7a64c05ffc03971
+PODFILE CHECKSUM: f2eb8f5a17c320a935d112236893d073c572e17e
COCOAPODS: 1.14.3
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 38f84fc654..fe3b934ce1 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 756BBC4C51FCB44047AB39E4 /* Pods_TeamMailShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -48,7 +49,6 @@
F5BBBF512B2EEC37007930E1 /* NetworkExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF502B2EEC37007930E1 /* NetworkExceptions.swift */; };
F5BBBF532B2EECAA007930E1 /* JmapExceptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF522B2EECAA007930E1 /* JmapExceptions.swift */; };
F5BBBF552B2EEF3D007930E1 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */; };
- F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EFC07C2B328B9F00829056 /* TwakeLogger.swift */; };
F5CFE0352B335322005A90A9 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D4EA022B2ABF090090DDFC /* NotificationService.swift */; };
F5D4EA072B2ABF090090DDFC /* TwakeMailNSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F5D4EA002B2ABF090090DDFC /* TwakeMailNSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -118,8 +118,11 @@
/* Begin PBXFileReference section */
06C73A54FA0478D3C89AB5E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.debug.xcconfig"; sourceTree = ""; };
+ 143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TeamMailShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.profile.xcconfig"; sourceTree = ""; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
4D124E74293A67D900BA5186 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; };
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -135,6 +138,7 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.release.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.release.xcconfig"; sourceTree = ""; };
F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSOTests.swift; sourceTree = ""; };
F522E8852C0EE8B600DDA35B /* CoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUtils.swift; sourceTree = ""; };
F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TeamMailShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -197,6 +201,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 756BBC4C51FCB44047AB39E4 /* Pods_TeamMailShareExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -223,6 +228,7 @@
isa = PBXGroup;
children = (
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */,
+ 143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */,
);
name = Frameworks;
sourceTree = "";
@@ -482,6 +488,9 @@
B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */,
631346BB444C71671599207F /* Pods-Runner.release.xcconfig */,
06C73A54FA0478D3C89AB5E5 /* Pods-Runner.profile.xcconfig */,
+ 07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */,
+ BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */,
+ 1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "";
@@ -510,8 +519,6 @@
F5D4EA062B2ABF090090DDFC /* PBXTargetDependency */,
);
name = Runner;
- packageProductDependencies = (
- );
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
@@ -520,6 +527,7 @@
isa = PBXNativeTarget;
buildConfigurationList = F52F993827FD6EB900346091 /* Build configuration list for PBXNativeTarget "TeamMailShareExtension" */;
buildPhases = (
+ E791D44F3C4EDE784D610B9E /* [CP] Check Pods Manifest.lock */,
F52F992927FD6EB900346091 /* Sources */,
F52F992A27FD6EB900346091 /* Frameworks */,
F52F992B27FD6EB900346091 /* Resources */,
@@ -696,7 +704,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n";
};
3D8E7FEF3D91E77326493F6D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
@@ -735,6 +743,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
+ E791D44F3C4EDE784D610B9E /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-TeamMailShareExtension-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -759,7 +789,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- F5CFE0302B335098005A90A9 /* TwakeLogger.swift in Sources */,
F52F993027FD6EB900346091 /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1120,7 +1149,9 @@
};
F52F993927FD6EB900346091 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */;
buildSettings = {
+ APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1160,7 +1191,9 @@
};
F52F993A27FD6EB900346091 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */;
buildSettings = {
+ APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
@@ -1197,7 +1230,9 @@
};
F52F993B27FD6EB900346091 /* Profile */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReference = 1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */;
buildSettings = {
+ APP_GROUP_ID = group.com.linagora.teammail;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CLANG_ENABLE_OBJC_WEAK = YES;
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/TeamMailShareExtension.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/TeamMailShareExtension.xcscheme
new file mode 100644
index 0000000000..5758ae89aa
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/TeamMailShareExtension.xcscheme
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/TeamMailShareExtension/Info.plist b/ios/TeamMailShareExtension/Info.plist
index 400268a436..adb158d904 100644
--- a/ios/TeamMailShareExtension/Info.plist
+++ b/ios/TeamMailShareExtension/Info.plist
@@ -2,6 +2,8 @@
+ AppGroupId
+ ${APP_GROUP_ID}
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
diff --git a/ios/TeamMailShareExtension/ShareViewController.swift b/ios/TeamMailShareExtension/ShareViewController.swift
index 5ece90a089..0e45452f8b 100644
--- a/ios/TeamMailShareExtension/ShareViewController.swift
+++ b/ios/TeamMailShareExtension/ShareViewController.swift
@@ -1,354 +1,4 @@
-import UIKit
-import Social
-import MobileCoreServices
-import Photos
+import receive_sharing_intent
-class ShareViewController: SLComposeServiceViewController {
- var hostAppBundleIdentifier = ""
- let appGroupId = "group.com.linagora.teammail"
- let sharedKey = "ShareKey"
- var sharedMedia: [SharedMediaFile] = []
- var sharedText: [String] = []
- let imageContentType = kUTTypeImage as String
- let textContentType = kUTTypeText as String
- let urlContentType = kUTTypeURL as String
- let videoContentType = kUTTypeMovie as String
- let fileURLType = kUTTypeFileURL as String
-
- override func isContentValid() -> Bool {
- return true
- }
-
- private func loadIds() {
- // loading Share extension App Id
- let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!;
-
-
- // convert ShareExtension id to host app id
- // By default it is remove last part of id after last point
- // For example: com.test.ShareExtension -> com.test
- let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: ".");
- hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[.. [Any]! {
- // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
- return []
- }
-
- private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
- attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in
-
- if error == nil, let item = data as? String, let this = self {
-
- this.sharedText.append(item)
-
- // If this is the last item, save imagesData in userDefaults and redirect to host app
- if index == (content.attachments?.count)! - 1 {
- let userDefaults = UserDefaults(suiteName: this.appGroupId)
- userDefaults?.set(this.sharedText, forKey: this.sharedKey)
- userDefaults?.synchronize()
- this.redirectToHostApp(type: .text)
- }
-
- } else {
- self?.dismissWithError()
- }
- }
- }
-
- private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
- attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in
-
- if error == nil, let item = data as? URL, let this = self {
-
- this.sharedText.append(item.absoluteString)
-
- // If this is the last item, save imagesData in userDefaults and redirect to host app
- if index == (content.attachments?.count)! - 1 {
- let userDefaults = UserDefaults(suiteName: this.appGroupId)
- userDefaults?.set(this.sharedText, forKey: this.sharedKey)
- userDefaults?.synchronize()
- this.redirectToHostApp(type: .text)
- }
-
- } else {
- self?.dismissWithError()
- }
- }
- }
-
- private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
- attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in
-
- if error == nil, let url = data as? URL, let this = self {
-
- // Always copy
- let fileName = this.getFileName(from: url, type: .image)
- let newPath = FileManager.default
- .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
- .appendingPathComponent(fileName)
- let copied = this.copyFile(at: url, to: newPath)
- if(copied) {
- this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image))
- }
-
- // If this is the last item, save imagesData in userDefaults and redirect to host app
- if index == (content.attachments?.count)! - 1 {
- let userDefaults = UserDefaults(suiteName: this.appGroupId)
- userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
- userDefaults?.synchronize()
- this.redirectToHostApp(type: .media)
- }
-
- } else {
- self?.dismissWithError()
- }
- }
- }
-
- private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
- attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in
-
- if error == nil, let url = data as? URL, let this = self {
-
- // Always copy
- let fileName = this.getFileName(from: url, type: .video)
- let newPath = FileManager.default
- .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
- .appendingPathComponent(fileName)
- let copied = this.copyFile(at: url, to: newPath)
- if(copied) {
- guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else {
- return
- }
- this.sharedMedia.append(sharedFile)
- }
-
- // If this is the last item, save imagesData in userDefaults and redirect to host app
- if index == (content.attachments?.count)! - 1 {
- let userDefaults = UserDefaults(suiteName: this.appGroupId)
- userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
- userDefaults?.synchronize()
- this.redirectToHostApp(type: .media)
- }
-
- } else {
- self?.dismissWithError()
- }
- }
- }
-
- private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) {
- attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in
-
- if error == nil, let url = data as? URL, let this = self {
-
- // Always copy
- let fileName = this.getFileName(from :url, type: .file)
- let newPath = FileManager.default
- .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)!
- .appendingPathComponent(fileName)
- let copied = this.copyFile(at: url, to: newPath)
- if (copied) {
- this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file))
- }
-
- if index == (content.attachments?.count)! - 1 {
- let userDefaults = UserDefaults(suiteName: this.appGroupId)
- userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey)
- userDefaults?.synchronize()
- this.redirectToHostApp(type: .file)
- }
-
- } else {
- self?.dismissWithError()
- }
- }
- }
-
-
- private func dismissWithError() {
- TwakeLogger.shared.log(message: "[ERROR] Error loading data!")
- let alert = UIAlertController(title: "Error", message: "Error loading data", preferredStyle: .alert)
-
- let action = UIAlertAction(title: "Error", style: .cancel) { _ in
- self.dismiss(animated: true, completion: nil)
- }
-
- alert.addAction(action)
- present(alert, animated: true, completion: nil)
- extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
- }
-
- private func redirectToHostApp(type: RedirectType) {
- // ids may not loaded yet so we need loadIds here too
- loadIds();
- let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://dataUrl=\(sharedKey)#\(type)")
- var responder = self as UIResponder?
- let selectorOpenURL = sel_registerName("openURL:")
-
- while (responder != nil) {
- if (responder?.responds(to: selectorOpenURL))! {
- let _ = responder?.perform(selectorOpenURL, with: url)
- }
- responder = responder!.next
- }
- extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
- }
-
- enum RedirectType {
- case media
- case text
- case file
- }
-
- func getExtension(from url: URL, type: SharedMediaType) -> String {
- let parts = url.lastPathComponent.components(separatedBy: ".")
- var ex: String? = nil
- if (parts.count > 1) {
- ex = parts.last
- }
-
- if (ex == nil) {
- switch type {
- case .image:
- ex = "PNG"
- case .video:
- ex = "MP4"
- case .file:
- ex = "TXT"
- }
- }
- return ex ?? "Unknown"
- }
-
- func getFileName(from url: URL, type: SharedMediaType) -> String {
- var name = url.lastPathComponent
-
- if (name.isEmpty) {
- name = UUID().uuidString + "." + getExtension(from: url, type: type)
- }
-
- return name
- }
-
- func copyFile(at srcURL: URL, to dstURL: URL) -> Bool {
- do {
- if FileManager.default.fileExists(atPath: dstURL.path) {
- try FileManager.default.removeItem(at: dstURL)
- }
- try FileManager.default.copyItem(at: srcURL, to: dstURL)
- } catch (let error) {
- TwakeLogger.shared.log(message: "Cannot copy item at \(srcURL) to \(dstURL): \(error)")
- return false
- }
- return true
- }
-
- private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? {
- let asset = AVAsset(url: forVideo)
- let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded()
- let thumbnailPath = getThumbnailPath(for: forVideo)
-
- if FileManager.default.fileExists(atPath: thumbnailPath.path) {
- return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video)
- }
-
- var saved = false
- let assetImgGenerate = AVAssetImageGenerator(asset: asset)
- assetImgGenerate.appliesPreferredTrackTransform = true
- // let scale = UIScreen.main.scale
- assetImgGenerate.maximumSize = CGSize(width: 360, height: 360)
- do {
- let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil)
- try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath)
- saved = true
- } catch {
- saved = false
- }
-
- return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil
-
- }
-
- private func getThumbnailPath(for url: URL) -> URL {
- let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "")
- let path = FileManager.default
- .containerURL(forSecurityApplicationGroupIdentifier: appGroupId)!
- .appendingPathComponent("\(fileName).jpg")
- return path
- }
-
- class SharedMediaFile: Codable {
- var path: String; // can be image, video or url path. It can also be text content
- var thumbnail: String?; // video thumbnail
- var duration: Double?; // video duration in milliseconds
- var type: SharedMediaType;
-
-
- init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) {
- self.path = path
- self.thumbnail = thumbnail
- self.duration = duration
- self.type = type
- }
-
- // Debug method to print out SharedMediaFile details in the console
- func toString() {
- TwakeLogger.shared.log(message: "[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)")
- }
- }
-
- enum SharedMediaType: Int, Codable {
- case image
- case video
- case file
- }
-
- func toData(data: [SharedMediaFile]) -> Data {
- let encodedData = try? JSONEncoder().encode(data)
- return encodedData!
- }
-}
-
-extension Array {
- subscript (safe index: UInt) -> Element? {
- return Int(index) < count ? self[Int(index)] : nil
- }
+class ShareViewController: RSIShareViewController {
}
diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart
index 4ec33848a4..852b63873c 100644
--- a/lib/features/base/base_controller.dart
+++ b/lib/features/base/base_controller.dart
@@ -407,14 +407,32 @@ abstract class BaseController extends GetxController
FirebaseCapability.fcmIdentifier.isSupported(session, accountId) && AppConfig.fcmAvailable;
void goToLogin() {
- if (Get.currentRoute != AppRoutes.login) {
- pushAndPopAll(
- AppRoutes.login,
- arguments: LoginArguments(
- PlatformInfo.isMobile ? LoginFormType.dnsLookupForm : LoginFormType.none
- )
- );
+ if (PlatformInfo.isMobile) {
+ navigateToTwakeWelcomePage();
+ } else {
+ navigateToLoginPage();
+ }
+ }
+
+ void removeAllPageAndGoToLogin() {
+ if (PlatformInfo.isMobile) {
+ pushAndPopAll(AppRoutes.twakeWelcome);
+ } else {
+ navigateToLoginPage();
+ }
+ }
+
+ void navigateToTwakeWelcomePage() {
+ popAndPush(AppRoutes.twakeWelcome);
+ }
+
+ void navigateToLoginPage() {
+ if (Get.currentRoute == AppRoutes.login) {
+ return;
}
+ pushAndPopAll(
+ AppRoutes.login,
+ arguments: LoginArguments(LoginFormType.none));
}
void logout(Session? session, AccountId? accountId) async {
@@ -460,7 +478,7 @@ abstract class BaseController extends GetxController
Future clearDataAndGoToLoginPage() async {
log('$runtimeType::clearDataAndGoToLoginPage:');
await clearAllData();
- goToLogin();
+ removeAllPageAndGoToLogin();
}
Future clearAllData() async {
diff --git a/lib/features/base/mixin/popup_context_menu_action_mixin.dart b/lib/features/base/mixin/popup_context_menu_action_mixin.dart
index 9acdf7e97a..5060baf553 100644
--- a/lib/features/base/mixin/popup_context_menu_action_mixin.dart
+++ b/lib/features/base/mixin/popup_context_menu_action_mixin.dart
@@ -9,8 +9,15 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart';
mixin PopupContextMenuActionMixin {
- void openContextMenuAction(BuildContext context, List actionTiles, {Widget? cancelButton}) async {
- await (CupertinoActionSheetBuilder(context)
+ void openContextMenuAction(
+ BuildContext context,
+ List actionTiles,
+ {
+ Widget? cancelButton,
+ Key? key,
+ }
+ ) async {
+ await (CupertinoActionSheetBuilder(context, key: key)
..addTiles(actionTiles)
..addCancelButton(cancelButton ?? buildCancelButton(context)))
.show();
diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart
index fbfa118421..f0750cec70 100644
--- a/lib/features/base/reloadable/reloadable_controller.dart
+++ b/lib/features/base/reloadable/reloadable_controller.dart
@@ -36,7 +36,7 @@ abstract class ReloadableController extends BaseController {
goToLogin();
} else if (failure is GetSessionFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure');
- _handleGetSessionFailure(failure.exception);
+ handleGetSessionFailure(failure.exception);
} else if (failure is UpdateAccountCacheFailure) {
logError('$runtimeType::handleFailureViewState():Failure = $failure');
_handleUpdateAccountCacheCompleted(
@@ -127,7 +127,7 @@ abstract class ReloadableController extends BaseController {
consumeState(_getSessionInteractor.execute());
}
- void _handleGetSessionFailure(GetSessionFailure failure) {
+ void handleGetSessionFailure(GetSessionFailure failure) {
if (failure.exception is! BadCredentialsException) {
toastManager.showMessageFailure(failure);
}
diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart
index b3ec9a78df..4e843153dd 100644
--- a/lib/features/composer/presentation/composer_controller.dart
+++ b/lib/features/composer/presentation/composer_controller.dart
@@ -629,6 +629,16 @@ class ComposerController extends BaseController
isInitialRecipient.value = true;
toAddressExpandMode.value = ExpandMode.COLLAPSE;
}
+ if (arguments.cc?.isNotEmpty == true) {
+ listCcEmailAddress = arguments.cc!;
+ ccRecipientState.value = PrefixRecipientState.enabled;
+ ccAddressExpandMode.value = ExpandMode.COLLAPSE;
+ }
+ if (arguments.bcc?.isNotEmpty == true) {
+ bccRecipientState.value = PrefixRecipientState.enabled;
+ bccAddressExpandMode.value = ExpandMode.COLLAPSE;
+ listBccEmailAddress = arguments.bcc!;
+ }
_getEmailContentFromMailtoUri(arguments.body ?? '');
_updateStatusEmailSendButton();
break;
@@ -955,12 +965,15 @@ class ComposerController extends BaseController
}
Future _getContentInEditor() async {
- final htmlTextEditor = PlatformInfo.isWeb
- ? _textEditorWeb
- : await htmlEditorApi?.getText();
- if (htmlTextEditor?.isNotEmpty == true) {
- return htmlTextEditor!.removeEditorStartTag();
- } else {
+ try {
+ final htmlTextEditor = PlatformInfo.isWeb
+ ? _textEditorWeb
+ : await htmlEditorApi?.getText();
+ return htmlTextEditor?.isNotEmpty == true
+ ? htmlTextEditor!.removeEditorStartTag()
+ : '';
+ } catch (e) {
+ logError('ComposerController::_getContentInEditor:Exception = $e');
return '';
}
}
@@ -1726,10 +1739,15 @@ class ComposerController extends BaseController
if (bccRecipientState.value == PrefixRecipientState.disabled) {
bccRecipientState.value = PrefixRecipientState.enabled;
}
- listBccEmailAddress = listEmailAddress.toList();
+ if (composerArguments.value?.emailActionType == EmailActionType.composeFromMailtoUri) {
+ listBccEmailAddress = {...listEmailAddress, ...?composerArguments.value?.bcc}.toList();
+ } else {
+ listBccEmailAddress = listEmailAddress.toList();
+ }
toAddressExpandMode.value = ExpandMode.COLLAPSE;
ccAddressExpandMode.value = ExpandMode.COLLAPSE;
bccAddressExpandMode.value = ExpandMode.COLLAPSE;
+ bccAddressExpandMode.refresh();
_updateStatusEmailSendButton();
}
diff --git a/lib/features/composer/presentation/extensions/shared_media_file_extension.dart b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart
index 93f6caa37f..bdf157a56f 100644
--- a/lib/features/composer/presentation/extensions/shared_media_file_extension.dart
+++ b/lib/features/composer/presentation/extensions/shared_media_file_extension.dart
@@ -1,22 +1,16 @@
import 'dart:io';
-import 'package:core/utils/platform_info.dart';
import 'package:model/upload/file_info.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/file_extension.dart';
extension SharedMediaFileExtension on SharedMediaFile {
- File toFile() {
- if (PlatformInfo.isIOS) {
- final pathFile = type == SharedMediaType.FILE
- ? path.toString().replaceAll('file:/', '').replaceAll('%20', ' ')
- : path.toString().replaceAll('%20', ' ');
- return File(pathFile);
- } else {
- return File(path);
- }
- }
+ File toFile() => File(path);
- FileInfo toFileInfo({bool? isShared}) => toFile().toFileInfo(isInline: type == SharedMediaType.IMAGE, isShared: isShared);
+ FileInfo toFileInfo({bool? isShared}) =>
+ toFile().toFileInfo(
+ isInline: type == SharedMediaType.image,
+ isShared: isShared,
+ );
}
\ No newline at end of file
diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart
index 2b9e09d765..e40e78fca3 100644
--- a/lib/features/email/presentation/controller/single_email_controller.dart
+++ b/lib/features/email/presentation/controller/single_email_controller.dart
@@ -1159,13 +1159,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin {
}
Future openMailToLink(Uri? uri) async {
+ if (uri == null) return;
+
+ final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(uri.toString());
log('SingleEmailController::openMailToLink(): ${uri.toString()}');
- String address = uri?.path ?? '';
- log('SingleEmailController::openMailToLink(): address: $address');
- if (address.isNotEmpty) {
- final emailAddress = EmailAddress(null, address);
- mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress));
- }
+ if (!RouteUtils.canOpenComposerFromNavigationRouter(navigationRouter)) return;
+
+ mailboxDashBoardController.goToComposer(
+ ComposerArguments.fromMailtoUri(
+ listEmailAddress: navigationRouter.listEmailAddress,
+ cc: navigationRouter.cc,
+ bcc: navigationRouter.bcc,
+ subject: navigationRouter.subject,
+ body: navigationRouter.body
+ )
+ );
}
void deleteEmailPermanently(BuildContext context, PresentationEmail email) {
diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart
index fc9b9f2811..479d0e617a 100644
--- a/lib/features/email/presentation/model/composer_arguments.dart
+++ b/lib/features/email/presentation/model/composer_arguments.dart
@@ -30,6 +30,8 @@ class ComposerArguments extends RouterArguments {
final List? inlineImages;
final bool? hasRequestReadReceipt;
final ScreenDisplayMode displayMode;
+ final List? cc;
+ final List? bcc;
ComposerArguments({
this.emailActionType = EmailActionType.compose,
@@ -49,7 +51,9 @@ class ComposerArguments extends RouterArguments {
this.selectedIdentityId,
this.inlineImages,
this.hasRequestReadReceipt,
- this.displayMode = ScreenDisplayMode.normal
+ this.displayMode = ScreenDisplayMode.normal,
+ this.cc,
+ this.bcc,
});
factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) =>
@@ -76,13 +80,20 @@ class ComposerArguments extends RouterArguments {
listEmailAddress: [emailAddress]
);
- factory ComposerArguments.fromMailtoUri({List? listEmailAddress, String? subject, String? body}) =>
- ComposerArguments(
- emailActionType: EmailActionType.composeFromMailtoUri,
- listEmailAddress: listEmailAddress,
- subject: subject,
- body: body,
- );
+ factory ComposerArguments.fromMailtoUri({
+ List? listEmailAddress,
+ String? subject,
+ String? body,
+ List? cc,
+ List? bcc
+ }) => ComposerArguments(
+ emailActionType: EmailActionType.composeFromMailtoUri,
+ listEmailAddress: listEmailAddress,
+ subject: subject,
+ body: body,
+ cc: cc,
+ bcc: bcc,
+ );
factory ComposerArguments.editDraftEmail(PresentationEmail presentationEmail) =>
ComposerArguments(
@@ -193,6 +204,8 @@ class ComposerArguments extends RouterArguments {
inlineImages,
hasRequestReadReceipt,
displayMode,
+ cc,
+ bcc,
];
ComposerArguments copyWith({
@@ -214,6 +227,8 @@ class ComposerArguments extends RouterArguments {
List? inlineImages,
bool? hasRequestReadReceipt,
ScreenDisplayMode? displayMode,
+ List? cc,
+ List? bcc,
}) {
return ComposerArguments(
emailActionType: emailActionType ?? this.emailActionType,
@@ -234,6 +249,8 @@ class ComposerArguments extends RouterArguments {
inlineImages: inlineImages ?? this.inlineImages,
hasRequestReadReceipt: hasRequestReadReceipt ?? this.hasRequestReadReceipt,
displayMode: displayMode ?? this.displayMode,
+ cc: cc ?? this.cc,
+ bcc: bcc ?? this.bcc,
);
}
}
diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart
index 4f6758cddd..b08595cab5 100644
--- a/lib/features/home/presentation/home_controller.dart
+++ b/lib/features/home/presentation/home_controller.dart
@@ -1,12 +1,7 @@
import 'package:core/utils/platform_info.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
-import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
-import 'package:jmap_dart_client/jmap/mail/email/email_address.dart';
-import 'package:model/email/email_content.dart';
-import 'package:model/email/email_content_type.dart';
-import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart';
import 'package:tmail_ui_user/features/cleanup/domain/model/cleanup_rule.dart';
@@ -45,7 +40,7 @@ class HomeController extends ReloadableController {
void onInit() {
if (PlatformInfo.isMobile) {
_initFlutterDownloader();
- _registerReceivingSharingIntent();
+ _registerReceivingFileSharing();
}
if (PlatformInfo.isIOS) {
_registerNotificationClickOnIOS();
@@ -85,20 +80,8 @@ class HomeController extends ReloadableController {
], eagerError: true).then((_) => getAuthenticatedAccountAction());
}
- void _registerReceivingSharingIntent() {
- _emailReceiveManager.receivingSharingStream.listen((uri) {
- if (uri != null) {
- if (GetUtils.isEmail(uri.path)) {
- _emailReceiveManager.setPendingEmailAddress(EmailAddress(null, uri.path));
- } else if (uri.scheme == "file") {
- _emailReceiveManager.setPendingFileInfo([SharedMediaFile(uri.path, null, null, SharedMediaType.FILE)]);
- } else {
- _emailReceiveManager.setPendingEmailContent(EmailContent(EmailContentType.textPlain, Uri.decodeComponent(uri.path)));
- }
- }
- });
-
- _emailReceiveManager.receivingFileSharingStream.listen(_emailReceiveManager.setPendingFileInfo);
+ void _registerReceivingFileSharing() {
+ _emailReceiveManager.registerReceivingFileSharingStreamWhileAppClosed();
}
void _registerNotificationClickOnIOS() {
diff --git a/lib/features/login/data/network/authentication_client/authentication_client_base.dart b/lib/features/login/data/network/authentication_client/authentication_client_base.dart
index a1a097644b..e3f9c70d4a 100644
--- a/lib/features/login/data/network/authentication_client/authentication_client_base.dart
+++ b/lib/features/login/data/network/authentication_client/authentication_client_base.dart
@@ -30,5 +30,9 @@ abstract class AuthenticationClientBase {
Future logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery);
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+
factory AuthenticationClientBase({String? tag}) => getAuthenticationClientImplementation(tag: tag);
}
\ No newline at end of file
diff --git a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart
index f22968b8a1..e307810b54 100644
--- a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart
+++ b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart
@@ -1,6 +1,7 @@
import 'package:core/utils/app_logger.dart';
import 'package:flutter_appauth/flutter_appauth.dart';
+import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:get/get.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:model/oidc/response/oidc_discovery_response.dart';
@@ -9,6 +10,7 @@ import 'package:model/oidc/token_oidc.dart';
import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart';
import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.dart';
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
+import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
@@ -96,6 +98,32 @@ class AuthenticationClientMobile implements AuthenticationClientBase {
Future getAuthenticationInfo() {
return Future.value('');
}
+
+ @override
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration) async {
+ final uri = await FlutterWebAuth2.authenticate(
+ url: oidcConfiguration.signInTWPUrl,
+ callbackUrlScheme: OIDCConstant.twakeWorkplaceUrlScheme,
+ options: const FlutterWebAuth2Options(
+ intentFlags: ephemeralIntentFlags,
+ ),
+ );
+ log('AuthenticationClientMobile::signInTwakeWorkplace():Uri = $uri');
+ return TokenOIDC.fromUri(uri);
+ }
+
+ @override
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration) async {
+ final uri = await FlutterWebAuth2.authenticate(
+ url: oidcConfiguration.signUpTWPUrl,
+ callbackUrlScheme: OIDCConstant.twakeWorkplaceUrlScheme,
+ options: const FlutterWebAuth2Options(
+ intentFlags: ephemeralIntentFlags,
+ ),
+ );
+ log('AuthenticationClientMobile::signUpTwakeWorkplace():Uri = $uri');
+ return TokenOIDC.fromUri(uri);
+ }
}
AuthenticationClientBase getAuthenticationClientImplementation({String? tag}) =>
diff --git a/lib/features/login/data/network/authentication_client/authentication_client_web.dart b/lib/features/login/data/network/authentication_client/authentication_client_web.dart
index 463f79a11e..1e3146a00c 100644
--- a/lib/features/login/data/network/authentication_client/authentication_client_web.dart
+++ b/lib/features/login/data/network/authentication_client/authentication_client_web.dart
@@ -106,6 +106,16 @@ class AuthenticationClientWeb implements AuthenticationClientBase {
throw CanNotAuthenticationInfoOnWeb();
}
}
+
+ @override
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ throw UnimplementedError();
+ }
}
AuthenticationClientBase getAuthenticationClientImplementation({String? tag}) =>
diff --git a/lib/features/login/data/network/config/oidc_constant.dart b/lib/features/login/data/network/config/oidc_constant.dart
index 8940791367..2e4790c58b 100644
--- a/lib/features/login/data/network/config/oidc_constant.dart
+++ b/lib/features/login/data/network/config/oidc_constant.dart
@@ -1,4 +1,4 @@
-import 'package:core/core.dart';
+import 'package:core/utils/platform_info.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
class OIDCConstant {
@@ -6,6 +6,11 @@ class OIDCConstant {
static List get oidcScope => ['openid', 'profile', 'email', 'offline_access'];
static const keyAuthorityOidc = 'KEY_AUTHORITY_OIDC';
static const authResponseKey = "auth_info";
+ static const String twakeWorkplaceUrlScheme = 'twakemail.mobile';
+ static const String twakeWorkplaceRedirectUrl = '$twakeWorkplaceUrlScheme://redirect';
+ static const String appParameter = 'tmail';
+ static const String postRegisteredRedirectUrlPathParams = 'post_registered_redirect_url';
+ static const String postLoginRedirectUrlPathParams = 'post_login_redirect_url';
static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId;
}
\ No newline at end of file
diff --git a/lib/features/login/domain/exceptions/authentication_exception.dart b/lib/features/login/domain/exceptions/authentication_exception.dart
index 2fd97c29e8..170b5e6d29 100644
--- a/lib/features/login/domain/exceptions/authentication_exception.dart
+++ b/lib/features/login/domain/exceptions/authentication_exception.dart
@@ -44,4 +44,8 @@ class CanNotFoundPassword implements Exception {}
class CanNotAuthenticationInfoOnWeb implements Exception {}
-class NotFoundAuthenticationInfoCache implements Exception {}
\ No newline at end of file
+class NotFoundAuthenticationInfoCache implements Exception {}
+
+class CanNotFoundSaasServerUrl implements Exception {}
+
+class SaasServerUriIsNull implements Exception {}
\ No newline at end of file
diff --git a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart
index fef03e30f7..9e4b60e79d 100644
--- a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart
+++ b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart
@@ -1,19 +1,23 @@
+import 'package:core/data/model/query/query_parameter.dart';
+import 'package:core/data/network/config/service_path.dart';
import 'package:core/utils/platform_info.dart';
import 'package:model/oidc/oidc_configuration.dart';
+import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart';
+import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart';
import 'package:tmail_ui_user/main/utils/app_config.dart';
extension OidcConfigurationExtensions on OIDCConfiguration {
String get redirectUrl {
if (PlatformInfo.isWeb) {
- if (AppConfig.domainRedirectUrl.endsWith('/')) {
- return AppConfig.domainRedirectUrl + loginRedirectOidcWeb;
- } else {
- return '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb';
- }
+ return AppConfig.domainRedirectUrl.endsWith('/')
+ ? AppConfig.domainRedirectUrl + loginRedirectOidcWeb
+ : '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb';
} else {
- return redirectOidcMobile;
+ return _isSaasAuthority(authority)
+ ? OIDCConstant.twakeWorkplaceRedirectUrl
+ : redirectOidcMobile;
}
}
@@ -25,7 +29,32 @@ extension OidcConfigurationExtensions on OIDCConfiguration {
return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb';
}
} else {
- return redirectOidcMobile;
+ return _isSaasAuthority(authority)
+ ? OIDCConstant.twakeWorkplaceRedirectUrl
+ : redirectOidcMobile;
}
}
+
+ bool _isSaasAuthority(String authority) =>
+ authority == AppConfig.saasRegistrationUrl;
+
+ String get signInTWPUrl => ServicePath(authority)
+ .withQueryParameters([
+ StringQueryParameter(
+ OIDCConstant.postLoginRedirectUrlPathParams,
+ OIDCConstant.twakeWorkplaceRedirectUrl,
+ ),
+ StringQueryParameter('app', OIDCConstant.appParameter),
+ ])
+ .generateEndpointPath();
+
+ String get signUpTWPUrl => ServicePath(authority)
+ .withQueryParameters([
+ StringQueryParameter(
+ OIDCConstant.postRegisteredRedirectUrlPathParams,
+ OIDCConstant.twakeWorkplaceRedirectUrl,
+ ),
+ StringQueryParameter('app', OIDCConstant.appParameter),
+ ])
+ .generateEndpointPath();
}
\ No newline at end of file
diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart
index 9032bd5134..cc5c092f11 100644
--- a/lib/features/login/presentation/base_login_view.dart
+++ b/lib/features/login/presentation/base_login_view.dart
@@ -27,7 +27,7 @@ abstract class BaseLoginView extends GetWidget {
side: const BorderSide(width: 0, color: AppColor.primaryColor)
)
),
- onPressed: controller.handleLoginPressed,
+ onPressed: () => controller.handleLoginPressed(context),
child: Text(
AppLocalizations.of(context).signIn,
style: const TextStyle(fontSize: 16, color: Colors.white)
@@ -89,7 +89,7 @@ abstract class BaseLoginView extends GetWidget {
hintText: AppLocalizations.of(context).password,
focusNode: controller.passFocusNode,
onTextChange: controller.onPasswordChange,
- onSubmitted: (_) => controller.handleLoginPressed(),
+ onSubmitted: (_) => controller.handleLoginPressed(context),
);
}
diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart
index 980cc98e7e..2dd140624d 100644
--- a/lib/features/login/presentation/login_bindings.dart
+++ b/lib/features/login/presentation/login_bindings.dart
@@ -6,6 +6,7 @@ import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cac
import 'package:tmail_ui_user/features/login/data/datasource/login_datasource.dart';
import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_login_datasource_impl.dart';
import 'package:tmail_ui_user/features/login/data/datasource_impl/login_datasource_impl.dart';
+import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
import 'package:tmail_ui_user/features/login/data/network/dns_service.dart';
import 'package:tmail_ui_user/features/login/data/repository/login_repository_impl.dart';
import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart';
@@ -26,6 +27,11 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_inte
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/presentation/login_controller.dart';
+import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
+import 'package:tmail_ui_user/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart';
+import 'package:tmail_ui_user/features/starting_page/data/repository/saas_authentication_repository_impl.dart';
+import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart';
+import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart';
import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart';
import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart';
@@ -47,12 +53,15 @@ class LoginBindings extends BaseBindings {
Get.find(),
Get.find(),
Get.find(),
+ Get.find(),
));
}
@override
void bindingsDataSource() {
Get.lazyPut(() => Get.find());
+ Get.lazyPut(
+ () => Get.find());
}
@override
@@ -66,6 +75,10 @@ class LoginBindings extends BaseBindings {
Get.find(),
Get.find(),
));
+ Get.lazyPut(() => SaasAuthenticationDataSourceImpl(
+ Get.find(),
+ Get.find(),
+ ));
}
@override
@@ -98,11 +111,19 @@ class LoginBindings extends BaseBindings {
Get.lazyPut(() => SaveLoginUsernameOnMobileInteractor(Get.find(),));
Get.lazyPut(() => GetAllRecentLoginUsernameOnMobileInteractor(Get.find()));
Get.lazyPut(() => DNSLookupToGetJmapUrlInteractor(Get.find()));
+ Get.lazyPut(() => SignInTwakeWorkplaceInteractor(
+ Get.find(),
+ Get.find(),
+ Get.find(),
+ Get.find(),
+ ));
}
@override
void bindingsRepository() {
Get.lazyPut(() => Get.find());
+ Get.lazyPut(
+ () => Get.find());
}
@override
@@ -113,5 +134,7 @@ class LoginBindings extends BaseBindings {
DataSourceType.network: Get.find(),
}
));
+ Get.lazyPut(() => SaasAuthenticationRepositoryImpl(
+ Get.find()));
}
}
\ No newline at end of file
diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart
index 425e0f0b6e..7946426e30 100644
--- a/lib/features/login/presentation/login_controller.dart
+++ b/lib/features/login/presentation/login_controller.dart
@@ -1,6 +1,7 @@
import 'package:core/presentation/extensions/url_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
+import 'package:core/presentation/utils/keyboard_utils.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/platform_info.dart';
import 'package:dartz/dartz.dart';
@@ -13,6 +14,7 @@ import 'package:model/account/password.dart';
import 'package:model/oidc/oidc_configuration.dart';
import 'package:model/oidc/request/oidc_request.dart';
import 'package:model/oidc/response/oidc_response.dart';
+import 'package:model/oidc/token_oidc.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart';
import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart';
@@ -48,6 +50,8 @@ import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_m
import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart';
import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart';
import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart';
+import 'package:tmail_ui_user/features/starting_page/domain/state/sign_in_twake_workplace_state.dart';
+import 'package:tmail_ui_user/features/starting_page/domain/usecase/sign_in_twake_workplace_interactor.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
import 'package:tmail_ui_user/main/routes/route_utils.dart';
@@ -69,6 +73,7 @@ class LoginController extends ReloadableController {
final SaveLoginUsernameOnMobileInteractor _saveLoginUsernameOnMobileInteractor;
final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor;
final DNSLookupToGetJmapUrlInteractor _dnsLookupToGetJmapUrlInteractor;
+ final SignInTwakeWorkplaceInteractor _signInTwakeWorkplaceInteractor;
final TextEditingController urlInputController = TextEditingController();
final TextEditingController usernameInputController = TextEditingController();
@@ -98,6 +103,7 @@ class LoginController extends ReloadableController {
this._saveLoginUsernameOnMobileInteractor,
this._getAllRecentLoginUsernameOnMobileInteractor,
this._dnsLookupToGetJmapUrlInteractor,
+ this._signInTwakeWorkplaceInteractor,
);
@override
@@ -129,7 +135,8 @@ class LoginController extends ReloadableController {
_handleCheckOIDCIsAvailableFailure(failure);
} else if (failure is GetStoredOidcConfigurationFailure ||
failure is GetOIDCIsAvailableFailure ||
- failure is GetOIDCConfigurationFailure
+ failure is GetOIDCConfigurationFailure ||
+ failure is SignInTwakeWorkplaceFailure
) {
_handleCommonOIDCFailure();
} else if (failure is GetTokenOIDCFailure) {
@@ -167,6 +174,12 @@ class LoginController extends ReloadableController {
_loginSuccessAction(success);
} else if (success is DNSLookupToGetJmapUrlSuccess) {
_handleDNSLookupToGetJmapUrlSuccess(success);
+ } else if (success is SignInTwakeWorkplaceSuccess) {
+ _synchronizeTokenAndGetSession(
+ baseUri: success.baseUri,
+ tokenOIDC: success.tokenOIDC,
+ oidcConfiguration: success.oidcConfiguration,
+ );
} else {
super.handleSuccessViewState(success);
}
@@ -179,7 +192,9 @@ class LoginController extends ReloadableController {
_handleCheckOIDCIsAvailableFailure(failure);
} else if (failure is GetStoredOidcConfigurationFailure ||
failure is GetOIDCConfigurationFailure ||
- failure is GetOIDCIsAvailableFailure) {
+ failure is GetOIDCIsAvailableFailure ||
+ failure is SignInTwakeWorkplaceFailure
+ ) {
_handleCommonOIDCFailure();
} else if (failure is GetTokenOIDCFailure) {
_handleNoSuitableBrowserOIDC(failure)
@@ -254,20 +269,30 @@ class LoginController extends ReloadableController {
));
}
- void handleBackButtonAction() {
+ void handleBackButtonAction(BuildContext context) {
+ KeyboardUtils.hideKeyboard(context);
clearState();
- if (loginFormType.value == LoginFormType.credentialForm) {
- _password = null;
- _username = null;
- usernameInputController.clear();
- passwordInputController.clear();
- loginFormType.value = LoginFormType.baseUrlForm;
- } else if (loginFormType.value == LoginFormType.passwordForm) {
- _password = null;
- _baseUri = null;
- urlInputController.clear();
- passwordInputController.clear();
- loginFormType.value = LoginFormType.dnsLookupForm;
+ switch(loginFormType.value) {
+ case LoginFormType.dnsLookupForm:
+ case LoginFormType.baseUrlForm:
+ navigateToTwakeWelcomePage();
+ break;
+ case LoginFormType.passwordForm:
+ _password = null;
+ _baseUri = null;
+ urlInputController.clear();
+ passwordInputController.clear();
+ loginFormType.value = LoginFormType.dnsLookupForm;
+ break;
+ case LoginFormType.credentialForm:
+ _password = null;
+ _username = null;
+ usernameInputController.clear();
+ passwordInputController.clear();
+ loginFormType.value = LoginFormType.baseUrlForm;
+ break;
+ default:
+ break;
}
}
@@ -277,7 +302,8 @@ class LoginController extends ReloadableController {
userNameFocusNode.requestFocus();
}
- void handleLoginPressed() {
+ void handleLoginPressed(BuildContext context) {
+ KeyboardUtils.hideKeyboard(context);
log('LoginController::handleLoginPressed:_currentBaseUrl: $_currentBaseUrl | _username: $_username | _password: $_password');
if (_currentBaseUrl == null) {
consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundBaseUrl()))));
@@ -314,11 +340,41 @@ class LoginController extends ReloadableController {
void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) {
if (PlatformInfo.isWeb) {
_authenticateOidcOnBrowserAction(success.oidcConfiguration);
+ } else if (success.oidcConfiguration.authority == AppConfig.saasRegistrationUrl) {
+ _getTokenOIDCOnSaaSPlatform(success.oidcConfiguration);
} else {
_getTokenOIDCAction(success.oidcConfiguration);
}
}
+ void _getTokenOIDCOnSaaSPlatform(OIDCConfiguration oidcConfiguration) {
+ if (_currentBaseUrl != null) {
+ consumeState(_signInTwakeWorkplaceInteractor.execute(
+ baseUri: _currentBaseUrl!,
+ oidcConfiguration: oidcConfiguration,
+ ));
+ } else {
+ dispatchState(Left(GetTokenOIDCFailure(CanNotFoundBaseUrl())));
+ }
+ }
+
+ void _synchronizeTokenAndGetSession({
+ required Uri baseUri,
+ required TokenOIDC tokenOIDC,
+ required OIDCConfiguration oidcConfiguration,
+ }) {
+ dynamicUrlInterceptors.setJmapUrl(baseUri.toString());
+ dynamicUrlInterceptors.changeBaseUrl(baseUri.toString());
+ authorizationInterceptors.setTokenAndAuthorityOidc(
+ newToken: tokenOIDC,
+ newConfig: oidcConfiguration);
+ authorizationIsolateInterceptors.setTokenAndAuthorityOidc(
+ newToken: tokenOIDC,
+ newConfig: oidcConfiguration);
+
+ getSessionAction();
+ }
+
void _getTokenOIDCAction(OIDCConfiguration config) {
if (_currentBaseUrl != null) {
consumeState(_getTokenOIDCInteractor.execute(_currentBaseUrl!, config));
@@ -345,15 +401,11 @@ class LoginController extends ReloadableController {
}
void _getTokenOIDCSuccess(GetTokenOIDCSuccess success) {
- dynamicUrlInterceptors.setJmapUrl(_currentBaseUrl?.toString());
- dynamicUrlInterceptors.changeBaseUrl(_currentBaseUrl?.toString());
- authorizationInterceptors.setTokenAndAuthorityOidc(
- newToken: success.tokenOIDC,
- newConfig: success.configuration);
- authorizationIsolateInterceptors.setTokenAndAuthorityOidc(
- newToken: success.tokenOIDC,
- newConfig: success.configuration);
- getSessionAction();
+ _synchronizeTokenAndGetSession(
+ baseUri: _currentBaseUrl!,
+ tokenOIDC: success.tokenOIDC,
+ oidcConfiguration: success.configuration,
+ );
}
void _loginSuccessAction(AuthenticationUserSuccess success) {
@@ -419,6 +471,8 @@ class LoginController extends ReloadableController {
void invokeDNSLookupToGetJmapUrl() {
log('LoginController::invokeDNSLookupToGetJmapUrl:_username $_username');
+ FocusManager.instance.primaryFocus?.unfocus();
+
if (_username == null) {
consumeState(Stream.value(Left(AuthenticationUserFailure(CanNotFoundUserName()))));
} else {
@@ -503,6 +557,11 @@ class LoginController extends ReloadableController {
passwordInputController.clear();
}
+ bool get isBackButtonActivated =>
+ loginFormType.value == LoginFormType.dnsLookupForm ||
+ loginFormType.value == LoginFormType.passwordForm ||
+ loginFormType.value == LoginFormType.credentialForm;
+
@override
void onClose() {
passFocusNode.dispose();
diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart
index a09e6cbe9a..bb051da49c 100644
--- a/lib/features/login/presentation/login_view.dart
+++ b/lib/features/login/presentation/login_view.dart
@@ -26,42 +26,50 @@ class LoginView extends BaseLoginView {
Widget build(BuildContext context) {
ThemeUtils.setSystemDarkUIStyle();
- return Scaffold(
- backgroundColor: AppColor.primaryLightColor,
- body: GestureDetector(
- onTap: () => FocusScope.of(context).unfocus(),
- child: Container(
- color: Colors.white,
- child: SafeArea(
- child: _supportScrollForm(context)
- ? Stack(children: [
- Center(
- child: SingleChildScrollView(
- scrollDirection: Axis.vertical,
- child: _buildCenterForm(context)
- )
- ),
- Obx(() {
- if (controller.loginFormType.value == LoginFormType.passwordForm ||
- controller.loginFormType.value == LoginFormType.credentialForm) {
- return LoginBackButton(onBackAction: controller.handleBackButtonAction);
- }
- return const SizedBox.shrink();
- })
- ])
- : Stack(children: [
- _buildCenterForm(context),
- Obx(() {
- if (controller.loginFormType.value == LoginFormType.passwordForm ||
- controller.loginFormType.value == LoginFormType.credentialForm) {
- return LoginBackButton(onBackAction: controller.handleBackButtonAction);
- }
- return const SizedBox.shrink();
- })
- ]),
+ return PopScope(
+ canPop: false,
+ onPopInvoked: (didPop) => !didPop
+ ? controller.handleBackButtonAction(context)
+ : null,
+ child: Scaffold(
+ backgroundColor: AppColor.primaryLightColor,
+ body: GestureDetector(
+ onTap: () => FocusScope.of(context).unfocus(),
+ child: Container(
+ color: Colors.white,
+ child: SafeArea(
+ child: _supportScrollForm(context)
+ ? Stack(children: [
+ Center(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.vertical,
+ child: _buildCenterForm(context)
+ )
+ ),
+ Obx(() {
+ if (controller.isBackButtonActivated) {
+ return LoginBackButton(
+ onBackAction: () => controller.handleBackButtonAction(context)
+ );
+ }
+ return const SizedBox.shrink();
+ })
+ ])
+ : Stack(children: [
+ _buildCenterForm(context),
+ Obx(() {
+ if (controller.isBackButtonActivated) {
+ return LoginBackButton(
+ onBackAction: () => controller.handleBackButtonAction(context)
+ );
+ }
+ return const SizedBox.shrink();
+ })
+ ]),
+ ),
),
- ),
- ));
+ )),
+ );
}
Widget _buildCenterForm(BuildContext context) {
@@ -103,7 +111,7 @@ class LoginView extends BaseLoginView {
textEditingController: controller.passwordInputController,
focusNode: controller.passFocusNode,
onTextChange: controller.onPasswordChange,
- onTextSubmitted: (_) => controller.handleLoginPressed(),
+ onTextSubmitted: (_) => controller.handleLoginPressed(context),
);
case LoginFormType.baseUrlForm:
return _buildUrlInput(context);
diff --git a/lib/features/login/presentation/privacy_link_widget.dart b/lib/features/login/presentation/privacy_link_widget.dart
index 5fe0b3c97e..9e6205140f 100644
--- a/lib/features/login/presentation/privacy_link_widget.dart
+++ b/lib/features/login/presentation/privacy_link_widget.dart
@@ -2,14 +2,13 @@ import 'package:core/presentation/extensions/color_extension.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
+import 'package:tmail_ui_user/main/utils/app_config.dart';
import 'package:tmail_ui_user/main/utils/app_utils.dart';
class PrivacyLinkWidget extends StatelessWidget {
- static const linagoraPrivacy = 'https://www.linagora.com/en/legal/privacy';
-
final String privacyUrlString;
- const PrivacyLinkWidget({Key? key, this.privacyUrlString = linagoraPrivacy}) : super(key: key);
+ const PrivacyLinkWidget({Key? key, this.privacyUrlString = AppConfig.linagoraPrivacyUrl}) : super(key: key);
@override
Widget build(BuildContext context) {
diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart
index 62b9027108..b0501879e4 100644
--- a/lib/features/mailbox/presentation/mailbox_controller.dart
+++ b/lib/features/mailbox/presentation/mailbox_controller.dart
@@ -460,6 +460,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM
mailboxDashBoardController.goToComposer(
ComposerArguments.fromMailtoUri(
listEmailAddress: _navigationRouter?.listEmailAddress,
+ cc: _navigationRouter?.cc,
+ bcc: _navigationRouter?.bcc,
subject: _navigationRouter?.subject,
body: _navigationRouter?.body
)
diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart
index 8c63db888f..c0fbf84c4b 100644
--- a/lib/features/mailbox/presentation/mailbox_view.dart
+++ b/lib/features/mailbox/presentation/mailbox_view.dart
@@ -192,7 +192,7 @@ class MailboxView extends BaseMailboxView {
);
}),
Obx(() => MailboxLoadingBarWidget(viewState: controller.viewState.value)),
- AppConfig.appGridDashboardAvailable
+ AppConfig.appGridDashboardAvailable && !PlatformInfo.isMobile
? buildAppGridDashboard(context, controller.responsiveUtils, controller.imagePaths, controller)
: const SizedBox.shrink(),
const SizedBox(height: 8),
diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
index edef20674f..bd5ca0ee12 100644
--- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
+++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart
@@ -25,6 +25,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart';
import 'package:model/model.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
+import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:rxdart/transformers.dart';
import 'package:tmail_ui_user/features/base/action/ui_action.dart';
import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart';
@@ -241,11 +242,8 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
ComposerArguments? composerArguments;
List? _identities;
ScrollController? listSearchFilterScrollController;
-
- late StreamSubscription _emailAddressStreamSubscription;
- late StreamSubscription _emailContentStreamSubscription;
- late StreamSubscription _fileReceiveManagerStreamSubscription;
-
+ StreamSubscription? _pendingSharedFileInfoSubscription;
+ StreamSubscription? _receivingFileSharingStreamSubscription;
StreamSubscription? _currentEmailIdInNotificationIOSStreamSubscription;
final StreamController> _progressStateController =
@@ -288,6 +286,9 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
@override
void onInit() {
+ if (PlatformInfo.isMobile) {
+ _registerReceivingFileSharingStream();
+ }
_registerStreamListener();
BackButtonInterceptor.add(_onBackButtonInterceptor, name: AppRoutes.dashboard);
WidgetsBinding.instance.addPostFrameCallback((_) async {
@@ -301,9 +302,6 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
if (PlatformInfo.isWeb) {
listSearchFilterScrollController = ScrollController();
}
- _registerPendingEmailAddress();
- _registerPendingEmailContents();
- _registerPendingFileInfo();
if (PlatformInfo.isIOS) {
_registerPendingCurrentEmailIdInNotification();
}
@@ -454,31 +452,85 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
}
}
- void _registerPendingEmailAddress() {
- _emailAddressStreamSubscription =
- _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) {
- if (emailAddress?.email?.isNotEmpty == true) {
- goToComposer(ComposerArguments.fromEmailAddress(emailAddress!));
- }
- });
- }
+ void _registerReceivingFileSharingStream() {
+ _receivingFileSharingStreamSubscription = _emailReceiveManager
+ .receivingFileSharingStream
+ .listen(
+ _emailReceiveManager.setPendingFileInfo,
+ onError: (err) {
+ logError('MailboxDashBoardController::_registerReceivingFileSharingStream::receivingFileSharingStream:Exception = $err');
+ },
+ );
- void _registerPendingEmailContents() {
- _emailContentStreamSubscription =
- _emailReceiveManager.pendingEmailContentInfo.stream.listen((emailContent) {
- if (emailContent?.content.isNotEmpty == true) {
- goToComposer(ComposerArguments.fromContentShared([emailContent!].asHtmlString));
- }
- });
+ _pendingSharedFileInfoSubscription = _emailReceiveManager
+ .pendingSharedFileInfo
+ .listen(
+ _handleReceivingFileSharing,
+ onError: (err) {
+ logError('MailboxDashBoardController::_registerReceivingFileSharingStream::pendingSharedFileInfo:Exception = $err');
+ },
+ );
}
- void _registerPendingFileInfo() {
- _fileReceiveManagerStreamSubscription =
- _emailReceiveManager.pendingFileInfo.stream.listen((listFile) {
- if (listFile.isNotEmpty) {
- goToComposer(ComposerArguments.fromFileShared(listFile));
- }
- });
+ void _handleReceivingFileSharing(List listSharedMediaFile) {
+ log('MailboxDashBoardController::_handleReceivingFileSharing: LIST_LENGTH = ${listSharedMediaFile.length}');
+ if (listSharedMediaFile.isEmpty) return;
+
+ for (var file in listSharedMediaFile) {
+ log('MailboxDashBoardController::_handleReceivingFileSharing:SharedMediaFile = ${file.toMap()}');
+ }
+
+ if (listSharedMediaFile.length == 1) {
+ final sharedMediaFile = listSharedMediaFile.first;
+ if (sharedMediaFile.path.trim().isEmpty) return;
+
+ switch (sharedMediaFile.type) {
+ case SharedMediaType.image:
+ case SharedMediaType.video:
+ case SharedMediaType.file:
+ goToComposer(
+ ComposerArguments.fromFileShared([sharedMediaFile]),
+ );
+ break;
+ case SharedMediaType.text:
+ if (sharedMediaFile.mimeType == Constant.textVCardMimeType) {
+ goToComposer(
+ ComposerArguments.fromFileShared([sharedMediaFile]),
+ );
+ } else if (sharedMediaFile.mimeType == Constant.textPlainMimeType) {
+ goToComposer(
+ ComposerArguments.fromContentShared(sharedMediaFile.path.trim()),
+ );
+ }
+ break;
+ case SharedMediaType.url:
+ if (sharedMediaFile.path.startsWith(RouteUtils.mailtoPrefix)) {
+ final navigationRouter = RouteUtils.generateNavigationRouterFromMailtoLink(sharedMediaFile.path);
+ goToComposer(
+ ComposerArguments.fromMailtoUri(
+ listEmailAddress: navigationRouter.listEmailAddress,
+ subject: navigationRouter.subject,
+ body: navigationRouter.body,
+ ),
+ );
+ }
+ break;
+ case SharedMediaType.mailto:
+ if (EmailUtils.isEmailAddressValid(sharedMediaFile.path)) {
+ goToComposer(
+ ComposerArguments.fromEmailAddress(
+ EmailAddress(null, sharedMediaFile.path),
+ ),
+ );
+ }
+ break;
+ }
+ return;
+ }
+
+ goToComposer(
+ ComposerArguments.fromFileShared(listSharedMediaFile),
+ );
}
void _registerPendingCurrentEmailIdInNotification() {
@@ -2861,14 +2913,15 @@ class MailboxDashBoardController extends ReloadableController with UserSettingPo
if (PlatformInfo.isWeb) {
listSearchFilterScrollController?.dispose();
}
- _emailReceiveManager.closeEmailReceiveManagerStream();
if (PlatformInfo.isIOS) {
_iosNotificationManager?.dispose();
_currentEmailIdInNotificationIOSStreamSubscription?.cancel();
}
- _emailAddressStreamSubscription.cancel();
- _emailContentStreamSubscription.cancel();
- _fileReceiveManagerStreamSubscription.cancel();
+ if (PlatformInfo.isMobile) {
+ _pendingSharedFileInfoSubscription?.cancel();
+ _receivingFileSharingStreamSubscription?.cancel();
+ _emailReceiveManager.closeEmailReceiveManagerStream();
+ }
_progressStateController.close();
_refreshActionEventController.close();
_notificationManager.closeStream();
diff --git a/lib/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart b/lib/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart
index a781452b2b..23172312b2 100644
--- a/lib/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart
+++ b/lib/features/mailbox_dashboard/presentation/model/search/email_sort_order_type.dart
@@ -15,21 +15,25 @@ enum EmailSortOrderType {
subjectDescending;
String getTitle(BuildContext context) {
+ return getTitleByAppLocalizations(AppLocalizations.of(context));
+ }
+
+ String getTitleByAppLocalizations(AppLocalizations appLocalizations) {
switch (this) {
case EmailSortOrderType.mostRecent:
- return AppLocalizations.of(context).mostRecent;
+ return appLocalizations.mostRecent;
case EmailSortOrderType.oldest:
- return AppLocalizations.of(context).oldest;
+ return appLocalizations.oldest;
case EmailSortOrderType.relevance:
- return AppLocalizations.of(context).relevance;
+ return appLocalizations.relevance;
case EmailSortOrderType.senderAscending:
- return AppLocalizations.of(context).senderAscending;
+ return appLocalizations.senderAscending;
case EmailSortOrderType.senderDescending:
- return AppLocalizations.of(context).senderDescending;
+ return appLocalizations.senderDescending;
case EmailSortOrderType.subjectAscending:
- return AppLocalizations.of(context).subjectAscending;
+ return appLocalizations.subjectAscending;
case EmailSortOrderType.subjectDescending:
- return AppLocalizations.of(context).subjectDescending;
+ return appLocalizations.subjectDescending;
}
}
diff --git a/lib/features/mailto/presentation/mailto_url_controller.dart b/lib/features/mailto/presentation/mailto_url_controller.dart
index a999b7e19f..29d3e1f134 100644
--- a/lib/features/mailto/presentation/mailto_url_controller.dart
+++ b/lib/features/mailto/presentation/mailto_url_controller.dart
@@ -28,7 +28,7 @@ class MailtoUrlController extends ReloadableController {
if (parameters.containsKey('uri')) {
final mailtoArgument = MailtoArguments(
session: session,
- mailtoUri: parameters['uri']
+ mailtoUri: Uri.base.toString(),
);
popAndPush(
RouteUtils.generateNavigationRoute(AppRoutes.dashboard),
diff --git a/lib/features/search/email/presentation/search_email_view.dart b/lib/features/search/email/presentation/search_email_view.dart
index 15d72ce66d..d90cdc3a4b 100644
--- a/lib/features/search/email/presentation/search_email_view.dart
+++ b/lib/features/search/email/presentation/search_email_view.dart
@@ -137,6 +137,7 @@ class SearchEmailView extends GetWidget
onTap: () => controller.closeSearchView(context: context)
),
Expanded(child: TextFieldBuilder(
+ key: const Key('search_email_text_field'),
onTextChange: controller.onTextSearchChange,
textInputAction: TextInputAction.search,
controller: controller.textInputSearchController,
@@ -190,6 +191,7 @@ class SearchEmailView extends GetWidget
),
scrollController: controller.listSearchFilterScrollController,
child: ListView(
+ key: const Key('search_filter_list_view'),
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: controller.listSearchFilterScrollController,
@@ -375,7 +377,8 @@ class SearchEmailView extends GetWidget
context,
controller.emailSortOrderType.value,
controller.selectSortOrderQuickSearchFilter
- )
+ ),
+ key: const Key('sort_filter_context_menu')
);
}
@@ -562,6 +565,7 @@ class SearchEmailView extends GetWidget
List listSuggestionSearch
) {
return ListView.builder(
+ key: const Key('suggestion_search_list_view'),
shrinkWrap: true,
primary: false,
itemCount: listSuggestionSearch.length,
@@ -618,6 +622,7 @@ class SearchEmailView extends GetWidget
Widget _buildListEmailBody(BuildContext context, List listPresentationEmail) {
return NotificationListener(
+ key: const Key('search_email_list_notification_listener'),
onNotification: (ScrollNotification scrollInfo) {
if (scrollInfo is ScrollEndNotification
&& controller.searchMoreState != SearchMoreState.waiting
diff --git a/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart b/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart
new file mode 100644
index 0000000000..93b31654b4
--- /dev/null
+++ b/lib/features/starting_page/data/datasource/saas_authentication_datasource.dart
@@ -0,0 +1,9 @@
+
+import 'package:model/oidc/oidc_configuration.dart';
+import 'package:model/oidc/token_oidc.dart';
+
+abstract class SaasAuthenticationDataSource {
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+}
\ No newline at end of file
diff --git a/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart b/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart
new file mode 100644
index 0000000000..1d59ec59ec
--- /dev/null
+++ b/lib/features/starting_page/data/datasource_impl/saas_authentication_datasource_impl.dart
@@ -0,0 +1,30 @@
+import 'package:model/oidc/oidc_configuration.dart';
+import 'package:model/oidc/token_oidc.dart';
+import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
+import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
+import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart';
+
+class SaasAuthenticationDataSourceImpl extends SaasAuthenticationDataSource {
+
+ final AuthenticationClientBase _authenticationClient;
+ final ExceptionThrower _exceptionThrower;
+
+ SaasAuthenticationDataSourceImpl(
+ this._authenticationClient,
+ this._exceptionThrower,
+ );
+
+ @override
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ return Future.sync(() async {
+ return await _authenticationClient.signInTwakeWorkplace(oidcConfiguration);
+ }).catchError(_exceptionThrower.throwException);
+ }
+
+ @override
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ return Future.sync(() async {
+ return await _authenticationClient.signUpTwakeWorkplace(oidcConfiguration);
+ }).catchError(_exceptionThrower.throwException);
+ }
+}
\ No newline at end of file
diff --git a/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart b/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart
new file mode 100644
index 0000000000..6ce779c8ac
--- /dev/null
+++ b/lib/features/starting_page/data/repository/saas_authentication_repository_impl.dart
@@ -0,0 +1,21 @@
+import 'package:model/oidc/oidc_configuration.dart';
+import 'package:model/oidc/token_oidc.dart';
+import 'package:tmail_ui_user/features/starting_page/data/datasource/saas_authentication_datasource.dart';
+import 'package:tmail_ui_user/features/starting_page/domain/repository/saas_authentication_repository.dart';
+
+class SaasAuthenticationRepositoryImpl extends SaasAuthenticationRepository {
+
+ final SaasAuthenticationDataSource _saasAuthenticationDataSource;
+
+ SaasAuthenticationRepositoryImpl(this._saasAuthenticationDataSource);
+
+ @override
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ return _saasAuthenticationDataSource.signInTwakeWorkplace(oidcConfiguration);
+ }
+
+ @override
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration) {
+ return _saasAuthenticationDataSource.signUpTwakeWorkplace(oidcConfiguration);
+ }
+}
\ No newline at end of file
diff --git a/lib/features/starting_page/domain/repository/saas_authentication_repository.dart b/lib/features/starting_page/domain/repository/saas_authentication_repository.dart
new file mode 100644
index 0000000000..a59a1b0ff1
--- /dev/null
+++ b/lib/features/starting_page/domain/repository/saas_authentication_repository.dart
@@ -0,0 +1,8 @@
+import 'package:model/oidc/oidc_configuration.dart';
+import 'package:model/oidc/token_oidc.dart';
+
+abstract class SaasAuthenticationRepository {
+ Future signInTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+
+ Future signUpTwakeWorkplace(OIDCConfiguration oidcConfiguration);
+}
\ No newline at end of file
diff --git a/lib/features/starting_page/domain/state/sign_in_twake_workplace_state.dart b/lib/features/starting_page/domain/state/sign_in_twake_workplace_state.dart
new file mode 100644
index 0000000000..eb8397575b
--- /dev/null
+++ b/lib/features/starting_page/domain/state/sign_in_twake_workplace_state.dart
@@ -0,0 +1,21 @@
+import 'package:core/presentation/state/failure.dart';
+import 'package:core/presentation/state/success.dart';
+import 'package:model/oidc/oidc_configuration.dart';
+import 'package:model/oidc/token_oidc.dart';
+
+class SignInTwakeWorkplaceLoading extends LoadingState {}
+
+class SignInTwakeWorkplaceSuccess extends Success {
+ final TokenOIDC tokenOIDC;
+ final Uri baseUri;
+ final OIDCConfiguration oidcConfiguration;
+
+ SignInTwakeWorkplaceSuccess(this.tokenOIDC, this.baseUri, this.oidcConfiguration);
+
+ @override
+ List