Skip to content

Commit

Permalink
TF-3278 Handle open app via deep link at MailboxDashboard screen
Browse files Browse the repository at this point in the history
  • Loading branch information
dab246 authored and hoangdat committed Dec 10, 2024
1 parent 3ecf9a8 commit b240937
Show file tree
Hide file tree
Showing 59 changed files with 1,070 additions and 426 deletions.
2 changes: 2 additions & 0 deletions core/lib/core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export 'presentation/extensions/string_extension.dart';
export 'presentation/extensions/tap_down_details_extension.dart';
export 'domain/extensions/media_type_extension.dart';
export 'presentation/extensions/map_extensions.dart';
export 'presentation/extensions/either_view_state_extension.dart';

// Exceptions
export 'domain/exceptions/download_file_exception.dart';
Expand Down Expand Up @@ -50,6 +51,7 @@ export 'utils/broadcast_channel/broadcast_channel.dart';
export 'utils/list_utils.dart';
export 'utils/mail/domain.dart';
export 'utils/mail/mail_address.dart';
export 'utils/application_manager.dart';

// Views
export 'presentation/views/text/slogan_builder.dart';
Expand Down
16 changes: 16 additions & 0 deletions core/lib/presentation/extensions/either_view_state_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:dartz/dartz.dart';

typedef OnFailureCallback = void Function(Failure? failure);
typedef OnSuccessCallback<T> = void Function(T success);

extension EitherViewStateExtension on Either<Failure, Success> {
void foldSuccess<T>({
required OnSuccessCallback<T> onSuccess,
required OnFailureCallback onFailure,
}) {
fold(onFailure,
(success) => success is T ? onSuccess(success as T) : onFailure(null));
}
}
17 changes: 15 additions & 2 deletions core/lib/utils/string_convert.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import 'dart:convert';

import 'package:core/utils/app_logger.dart';

class StringConvert {
static String? writeEmptyToNull(String text) {
static String? writeEmptyToNull(String text) {
if (text.isEmpty) return null;
return text;
}

static String writeNullToEmpty(String? text) {
static String writeNullToEmpty(String? text) {
return text ?? '';
}

static String decodeBase64ToString(String text) {
try {
return utf8.decode(base64Decode(text));
} catch (e) {
logError('StringConvert::decodeBase64ToString:Exception = $e');
return text;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:core/presentation/extensions/either_view_state_extension.dart';
import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:dartz/dartz.dart';

class MockFailure extends Failure {
final String message;

MockFailure(this.message);

@override
List<Object?> get props => [message];
}

class MockSuccess extends Success {
final String data;

MockSuccess(this.data);

@override
List<Object?> get props => [data];
}

class AnotherSuccess extends Success {
@override
List<Object?> get props => [];
}

void main() {
group('EitherViewStateExtension::foldSuccess::test', () {
test('Should calls onFailure when Either is Left', () {
final either = Left<Failure, Success>(MockFailure('Error occurred'));
bool failureCalled = false;

either.foldSuccess<Success>(
onSuccess: (_) => fail('onSuccess should not be called'),
onFailure: (failure) {
failureCalled = true;
expect(failure, isNotNull);
expect(failure, isA<MockFailure>());
},
);

expect(failureCalled, isTrue);
});

test('Should calls onSuccess when Either is Right with matching type', () {
final either = Right<Failure, Success>(MockSuccess('Successful'));
bool successCalled = false;

either.foldSuccess<Success>(
onSuccess: (success) {
successCalled = true;
expect(success, isNotNull);
expect(success, isA<MockSuccess>());
},
onFailure: (_) => fail('onFailure should not be called'),
);

expect(successCalled, isTrue);
});

test('Should calls onFailure when Either is Right with non-matching type', () {
final either = Right<Failure, Success>(MockSuccess('Successful'));
bool failureCalled = false;

either.foldSuccess<AnotherSuccess>(
onSuccess: (_) => fail('onSuccess should not be called'),
onFailure: (failure) {
failureCalled = true;
expect(failure, isNull);
},
);

expect(failureCalled, isTrue);
});
});
}
40 changes: 40 additions & 0 deletions core/test/utils/string_convert_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:core/utils/string_convert.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('StringConvert::decodeBase64ToString::test', () {
test('should decode a valid Base64 string to a normal string', () {
// Arrange
const base64Encoded = 'SGVsbG8gV29ybGQh';
const expectedDecoded = 'Hello World!';

// Act
final result = StringConvert.decodeBase64ToString(base64Encoded);

// Assert
expect(result, expectedDecoded);
});

test('should return the original string for invalid Base64 input', () {
// Arrange
const invalidBase64 = 'InvalidBase64@@';

// Act
final result = StringConvert.decodeBase64ToString(invalidBase64);

// Assert
expect(result, invalidBase64);
});

test('should return the original string for empty input', () {
// Arrange
const emptyInput = '';

// Act
final result = StringConvert.decodeBase64ToString(emptyInput);

// Assert
expect(result, emptyInput);
});
});
}
6 changes: 6 additions & 0 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
PODS:
- app_links (0.0.2):
- Flutter
- app_settings (5.1.1):
- Flutter
- AppAuth (1.7.4):
Expand Down Expand Up @@ -204,6 +206,7 @@ PODS:
- Flutter

DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- better_open_file (from `.symlinks/plugins/better_open_file/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
Expand Down Expand Up @@ -264,6 +267,8 @@ SPEC REPOS:
- UniversalDetector2

EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
app_settings:
:path: ".symlinks/plugins/app_settings/ios"
better_open_file:
Expand Down Expand Up @@ -334,6 +339,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"

SPEC CHECKSUMS:
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
app_settings: 017320c6a680cdc94c799949d95b84cb69389ebc
AppAuth: 182c5b88630569df5acb672720534756c29b3358
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
Expand Down
1 change: 1 addition & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>teammail.mobile</string>
<string>twakemail.mobile</string>
<string>mailto</string>
</array>
</dict>
Expand Down
4 changes: 3 additions & 1 deletion ios/TwakeCore/Jmap/JmapClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class JmapClient {
basicAuth: String?,
tokenEndpointUrl: String?,
oidcScopes: [String]?,
isTWP: Bool?,
onComplete: @escaping ([Email], [Error]) -> Void
) {
if (authenticationType == AuthenticationType.basic) {
Expand All @@ -42,7 +43,8 @@ class JmapClient {
tokenRefreshManager = TokenRefreshManager(
refreshToken: tokenOidc?.refreshToken ?? "",
tokenEndpoint: tokenEndpointUrl ?? "",
scopes: oidcScopes
scopes: oidcScopes,
isTWP: isTWP
)
}

Expand Down
15 changes: 13 additions & 2 deletions ios/TwakeCore/Network/TokenRefreshManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class TokenRefreshManager {
private let MOBIE_CLIENT_ID = "teammail-mobile"
private let MOBIE_REDIRECT_URL = "teammail.mobile://oauthredirect"
private let OIDC_SCOPES = ["openid", "profile", "email", "offline_access"]
private let TWP_MOBIE_REDIRECT_URL = "twakemail.mobile://redirect"

private let GRANT_TYPE = "grant_type"
private let REFRESH_TOKEN = "refresh_token"
Expand All @@ -15,11 +16,13 @@ class TokenRefreshManager {
let refreshToken: String
let tokenEndpoint: String
let scopes: [String]?
let isTWP: Bool?

init(refreshToken: String, tokenEndpoint: String, scopes: [String]?) {
init(refreshToken: String, tokenEndpoint: String, scopes: [String]?, isTWP: Bool?) {
self.refreshToken = refreshToken
self.tokenEndpoint = tokenEndpoint
self.scopes = scopes
self.isTWP = isTWP
}

private func getScopes() -> String {
Expand All @@ -28,6 +31,14 @@ class TokenRefreshManager {
}
return OIDC_SCOPES.joined(separator: " ")
}

private func getRedirectUrl() -> String {
if (isTWP == true) {
return TWP_MOBIE_REDIRECT_URL
} else {
return MOBIE_REDIRECT_URL
}
}

func handleRefreshAccessToken(
onSuccess: @escaping (TokenResponse) -> Void,
Expand All @@ -41,7 +52,7 @@ class TokenRefreshManager {
let params = [
CLIENT_ID: MOBIE_CLIENT_ID,
GRANT_TYPE: REFRESH_TOKEN,
REDIRECT_URI: MOBIE_REDIRECT_URL,
REDIRECT_URI: getRedirectUrl(),
REFRESH_TOKEN: refreshToken,
SCOPES: getScopes(),
]
Expand Down
7 changes: 5 additions & 2 deletions ios/TwakeMailNSE/Model/KeychainSharingSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct KeychainSharingSession: Codable {
let tokenEndpoint: String?
let oidcScopes: [String]?
let mailboxIdsBlockNotification: [String]?
let isTWP: Bool?
}

extension KeychainSharingSession {
Expand All @@ -27,7 +28,8 @@ extension KeychainSharingSession {
basicAuth: self.basicAuth,
tokenEndpoint: self.tokenEndpoint,
oidcScopes: self.oidcScopes,
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
isTWP: self.isTWP
)
}

Expand All @@ -48,7 +50,8 @@ extension KeychainSharingSession {
basicAuth: self.basicAuth,
tokenEndpoint: self.tokenEndpoint,
oidcScopes: self.oidcScopes,
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification
mailboxIdsBlockNotification: self.mailboxIdsBlockNotification,
isTWP: self.isTWP
)
}

Expand Down
1 change: 1 addition & 0 deletions ios/TwakeMailNSE/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class NotificationService: UNNotificationServiceExtension {
basicAuth: keychainSharingSession.basicAuth,
tokenEndpointUrl: keychainSharingSession.tokenEndpoint,
oidcScopes: keychainSharingSession.oidcScopes,
isTWP: keychainSharingSession.isTWP,
onComplete: { (emails, errors) in
do {
if emails.isEmpty {
Expand Down
Loading

0 comments on commit b240937

Please sign in to comment.