diff --git a/lib/features/home/domain/extensions/session_extensions.dart b/lib/features/home/domain/extensions/session_extensions.dart index 9ce56c48a..67af48f84 100644 --- a/lib/features/home/domain/extensions/session_extensions.dart +++ b/lib/features/home/domain/extensions/session_extensions.dart @@ -32,6 +32,7 @@ extension SessionExtensions on Session { tmailContactCapabilityIdentifier: AutocompleteCapability.deserialize, linagoraDownloadAllCapability: DownloadAllCapability.deserialize, capabilityServerSettings: SettingsCapability.deserialize, + linagoraSaaSCapability: SaaSAccountCapability.deserialize, }; Map toJson() { diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 380486c41..d0100d26e 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -51,7 +51,9 @@ class MailboxView extends BaseMailboxView { end: isDesktop ? 0 : 24, ), isDesktop: isDesktop, - onTapAction: () {}, + onTapAction: () => controller + .mailboxDashBoardController + .navigateToPaywall(context), ); } else { return const SizedBox.shrink(); diff --git a/lib/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart b/lib/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart index 3d86b73c3..59d462a99 100644 --- a/lib/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart +++ b/lib/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart @@ -6,7 +6,10 @@ import 'package:core/utils/app_logger.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/linagora_ecosystem_exceptions.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/api_url_linagora_ecosystem.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; class LinagoraEcosystemApi { final DioClient _dioClient; @@ -26,4 +29,33 @@ class LinagoraEcosystemApi { throw NotFoundLinagoraEcosystem(); } } + + Future getPaywallUrl(String baseUrl) async { + final result = await _dioClient.get( + Endpoint.linagoraEcosystem.usingBaseUrl(baseUrl).generateEndpointPath(), + ); + log('LinagoraEcosystemApi::getPaywallUrl: $result'); + + LinagoraEcosystem? linagoraEcosystem; + + if (result is Map) { + linagoraEcosystem = LinagoraEcosystem.deserialize(result); + } else if (result is String) { + linagoraEcosystem = LinagoraEcosystem.deserialize(jsonDecode(result)); + } + + if (linagoraEcosystem == null) { + throw NotFoundLinagoraEcosystem(); + } else { + final paywallProperty = + linagoraEcosystem.properties?[LinagoraEcosystemIdentifier.paywallURL] as ApiUrlLinagoraEcosystem?; + log('LinagoraEcosystemApi::getPaywallUrl: paywallProperty = $paywallProperty'); + + if (paywallProperty == null) { + throw NotFoundPaywallUrl(); + } else { + return PaywallUrlPattern(paywallProperty.value); + } + } + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/exceptions/linagora_ecosystem_exceptions.dart b/lib/features/mailbox_dashboard/domain/exceptions/linagora_ecosystem_exceptions.dart index edd236cc5..fe58d14cb 100644 --- a/lib/features/mailbox_dashboard/domain/exceptions/linagora_ecosystem_exceptions.dart +++ b/lib/features/mailbox_dashboard/domain/exceptions/linagora_ecosystem_exceptions.dart @@ -1,2 +1,4 @@ -class NotFoundLinagoraEcosystem implements Exception {} \ No newline at end of file +class NotFoundLinagoraEcosystem implements Exception {} + +class NotFoundPaywallUrl implements Exception {} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart index 8513f3b08..e031963ae 100644 --- a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart +++ b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/converters/linagora_ecosystem_converter.dart @@ -27,6 +27,7 @@ class LinagoraEcosystemConverter { LinagoraEcosystemIdentifier.twakeSync: AppLinagoraEcosystem.deserialize, LinagoraEcosystemIdentifier.linShare: AppLinagoraEcosystem.deserialize, LinagoraEcosystemIdentifier.mobileApps: MobileAppsLinagoraEcosystemConverter.deserialize, + LinagoraEcosystemIdentifier.paywallURL: ApiUrlLinagoraEcosystem.deserialize, }); } diff --git a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart index ed931e0c0..f4a5f38c4 100644 --- a/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart +++ b/lib/features/mailbox_dashboard/domain/linagora_ecosystem/linagora_ecosystem_identifier.dart @@ -11,6 +11,7 @@ class LinagoraEcosystemIdentifier with EquatableMixin { static final twakeChat = LinagoraEcosystemIdentifier('Twake Chat'); static final twakeSync = LinagoraEcosystemIdentifier('Twake Sync'); static final linShare = LinagoraEcosystemIdentifier('LinShare'); + static final paywallURL = LinagoraEcosystemIdentifier('paywallUrlTemplate'); final String value; diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 61a980ebe..60f0d0fda 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -109,6 +109,7 @@ import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_work import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/paywall/presentation/paywall_bindings.dart'; import 'package:tmail_ui_user/features/quotas/presentation/quotas_bindings.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; @@ -376,6 +377,7 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => GetIdentityCacheOnWebInteractor( Get.find() )); + PaywallBindings().dependencies(); } @override 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 52d85dc43..b6da6e069 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -127,6 +127,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/reopen_composer_cache_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/set_error_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/update_current_emails_flags_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/validate_saas_premium_available_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/web_auth_redirect_processor_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; @@ -152,6 +153,8 @@ import 'package:tmail_ui_user/features/manage_account/presentation/model/account import 'package:tmail_ui_user/features/manage_account/presentation/model/manage_account_arguments.dart'; import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; +import 'package:tmail_ui_user/features/paywall/domain/state/get_paywall_url_state.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/controller/web_socket_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; @@ -302,6 +305,7 @@ class MailboxDashBoardController extends ReloadableController StreamSubscription? _deepLinkDataStreamSubscription; int minInputLengthAutocomplete = AppConfig.defaultMinInputLengthAutocomplete; EmailSortOrderType currentSortOrder = SearchEmailFilter.defaultSortOrder; + PaywallUrlPattern? paywallUrlPattern; final StreamController> _progressStateController = StreamController>.broadcast(); @@ -497,6 +501,8 @@ class MailboxDashBoardController extends ReloadableController ); } else if (success is GetStoredEmailSortOrderSuccess) { setUpDefaultEmailSortOrder(success.emailSortOrderType); + } else if (success is GetPaywallUrlSuccess) { + loadPaywallUrlSuccess(success.paywallUrlPattern); } else { super.handleSuccessViewState(success); } @@ -830,6 +836,8 @@ class MailboxDashBoardController extends ReloadableController if (PlatformInfo.isMobile) { getAllSendingEmails(); _storeSessionAction(session); + } else { + loadPaywallUrl(); } } diff --git a/lib/features/mailbox_dashboard/presentation/extensions/validate_saas_premium_available_extension.dart b/lib/features/mailbox_dashboard/presentation/extensions/validate_saas_premium_available_extension.dart index dbd779522..de4043ac0 100644 --- a/lib/features/mailbox_dashboard/presentation/extensions/validate_saas_premium_available_extension.dart +++ b/lib/features/mailbox_dashboard/presentation/extensions/validate_saas_premium_available_extension.dart @@ -1,5 +1,11 @@ +import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/home/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; +import 'package:tmail_ui_user/features/paywall/domain/usecases/get_paywall_url_interactor.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; extension ValidateSaasPremiumAvailableExtension on MailboxDashBoardController { bool get isPremiumAvailable { @@ -13,4 +19,35 @@ extension ValidateSaasPremiumAvailableExtension on MailboxDashBoardController { return saasAccount?.isPremiumAvailable ?? false; } + + void loadPaywallUrl() { + final getPaywallUrlInteractor = getBinding(); + final jmapUrl = dynamicUrlInterceptors.jmapUrl; + + if (getPaywallUrlInteractor != null && jmapUrl != null) { + consumeState(getPaywallUrlInteractor.execute(jmapUrl)); + } else { + paywallUrlPattern = null; + } + } + + void loadPaywallUrlSuccess(PaywallUrlPattern newPattern) { + paywallUrlPattern = newPattern; + } + + void navigateToPaywall(BuildContext context) { + if (paywallUrlPattern == null) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).paywallUrlNotAvailable, + ); + return; + } + + final qualifiedPaywall = paywallUrlPattern!.getQualifiedUrl( + ownerEmail: ownEmailAddress.value, + ); + + AppUtils.launchLink(qualifiedPaywall); + } } diff --git a/lib/features/paywall/data/datasource/paywall_datasource.dart b/lib/features/paywall/data/datasource/paywall_datasource.dart new file mode 100644 index 000000000..ea283d946 --- /dev/null +++ b/lib/features/paywall/data/datasource/paywall_datasource.dart @@ -0,0 +1,5 @@ +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; + +abstract class PaywallDatasource { + Future getPaywallUrl(String baseUrl); +} diff --git a/lib/features/paywall/data/datasource_impl/paywall_datasource_impl.dart b/lib/features/paywall/data/datasource_impl/paywall_datasource_impl.dart new file mode 100644 index 000000000..4060ee7a2 --- /dev/null +++ b/lib/features/paywall/data/datasource_impl/paywall_datasource_impl.dart @@ -0,0 +1,18 @@ +import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart'; +import 'package:tmail_ui_user/features/paywall/data/datasource/paywall_datasource.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class PaywallDatasourceImpl extends PaywallDatasource { + final LinagoraEcosystemApi _linagoraEcosystemApi; + final ExceptionThrower _exceptionThrower; + + PaywallDatasourceImpl(this._linagoraEcosystemApi, this._exceptionThrower); + + @override + Future getPaywallUrl(String baseUrl) { + return Future.sync(() async { + return await _linagoraEcosystemApi.getPaywallUrl(baseUrl); + }).catchError(_exceptionThrower.throwException); + } +} diff --git a/lib/features/paywall/data/repository/paywall_repository_impl.dart b/lib/features/paywall/data/repository/paywall_repository_impl.dart new file mode 100644 index 000000000..5980abaf9 --- /dev/null +++ b/lib/features/paywall/data/repository/paywall_repository_impl.dart @@ -0,0 +1,14 @@ +import 'package:tmail_ui_user/features/paywall/data/datasource/paywall_datasource.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; +import 'package:tmail_ui_user/features/paywall/domain/repository/paywall_repository.dart'; + +class PaywallRepositoryImpl extends PaywallRepository { + final PaywallDatasource _paywallDatasource; + + PaywallRepositoryImpl(this._paywallDatasource); + + @override + Future getPaywallUrl(String baseUrl) { + return _paywallDatasource.getPaywallUrl(baseUrl); + } +} diff --git a/lib/features/paywall/domain/model/paywall_url_pattern.dart b/lib/features/paywall/domain/model/paywall_url_pattern.dart new file mode 100644 index 000000000..fcb7c2d1b --- /dev/null +++ b/lib/features/paywall/domain/model/paywall_url_pattern.dart @@ -0,0 +1,40 @@ +import 'package:core/utils/mail/domain.dart'; +import 'package:core/utils/mail/mail_address.dart'; +import 'package:equatable/equatable.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/paywall/presentation/paywall_utils.dart'; + +class PaywallUrlPattern with EquatableMixin { + final String pattern; + + PaywallUrlPattern(this.pattern); + + String getQualifiedUrl({required String ownerEmail, String? domainName}) { + final mailAddress = _getMailAddress(ownerEmail: ownerEmail); + return PaywallUtils.buildPaywallUrlFromTemplate( + template: pattern, + localPart: mailAddress?.localPart, + domainName: mailAddress?.domain.domainName, + ); + } + + MailAddress? _getMailAddress({required String ownerEmail}) { + try { + return MailAddress.validateAddress(ownerEmail); + } catch (e) { + if (GetUtils.isEmail(ownerEmail)) { + final listPart = ownerEmail.split('@'); + if (listPart.length == 2) { + return MailAddress( + localPart: listPart.first, + domain: Domain.of((listPart.last)), + ); + } + } + return null; + } + } + + @override + List get props => [pattern]; +} diff --git a/lib/features/paywall/domain/repository/paywall_repository.dart b/lib/features/paywall/domain/repository/paywall_repository.dart new file mode 100644 index 000000000..ba9c85f04 --- /dev/null +++ b/lib/features/paywall/domain/repository/paywall_repository.dart @@ -0,0 +1,5 @@ +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; + +abstract class PaywallRepository { + Future getPaywallUrl(String baseUrl); +} diff --git a/lib/features/paywall/domain/state/get_paywall_url_state.dart b/lib/features/paywall/domain/state/get_paywall_url_state.dart new file mode 100644 index 000000000..13ddff295 --- /dev/null +++ b/lib/features/paywall/domain/state/get_paywall_url_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/paywall/domain/model/paywall_url_pattern.dart'; + +class GettingPaywallUrl extends LoadingState {} + +class GetPaywallUrlSuccess extends UIState { + final PaywallUrlPattern paywallUrlPattern; + + GetPaywallUrlSuccess(this.paywallUrlPattern); + + @override + List get props => [paywallUrlPattern]; +} + +class GetPaywallUrlFailure extends FeatureFailure { + GetPaywallUrlFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/paywall/domain/usecases/get_paywall_url_interactor.dart b/lib/features/paywall/domain/usecases/get_paywall_url_interactor.dart new file mode 100644 index 000000000..bf2a0bb78 --- /dev/null +++ b/lib/features/paywall/domain/usecases/get_paywall_url_interactor.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/paywall/domain/repository/paywall_repository.dart'; +import 'package:tmail_ui_user/features/paywall/domain/state/get_paywall_url_state.dart'; + +class GetPaywallUrlInteractor { + final PaywallRepository _paywallRepository; + + GetPaywallUrlInteractor(this._paywallRepository); + + Stream> execute(String baseUrl) async* { + try { + yield Right(GettingPaywallUrl()); + final paywallUrl = await _paywallRepository.getPaywallUrl(baseUrl); + yield Right(GetPaywallUrlSuccess(paywallUrl)); + } catch (e) { + yield Left(GetPaywallUrlFailure(e)); + } + } +} diff --git a/lib/features/paywall/presentation/paywall_bindings.dart b/lib/features/paywall/presentation/paywall_bindings.dart new file mode 100644 index 000000000..4c560cdec --- /dev/null +++ b/lib/features/paywall/presentation/paywall_bindings.dart @@ -0,0 +1,41 @@ +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/linagora_ecosystem_api.dart'; +import 'package:tmail_ui_user/features/paywall/data/datasource/paywall_datasource.dart'; +import 'package:tmail_ui_user/features/paywall/data/datasource_impl/paywall_datasource_impl.dart'; +import 'package:tmail_ui_user/features/paywall/data/repository/paywall_repository_impl.dart'; +import 'package:tmail_ui_user/features/paywall/domain/repository/paywall_repository.dart'; +import 'package:tmail_ui_user/features/paywall/domain/usecases/get_paywall_url_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class PaywallBindings extends InteractorsBindings { + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut( + () => PaywallDatasourceImpl( + Get.find(), + Get.find(), + ), + ); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => GetPaywallUrlInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => PaywallRepositoryImpl(Get.find())); + } +} diff --git a/lib/features/paywall/presentation/paywall_utils.dart b/lib/features/paywall/presentation/paywall_utils.dart new file mode 100644 index 000000000..4c3aed65a --- /dev/null +++ b/lib/features/paywall/presentation/paywall_utils.dart @@ -0,0 +1,29 @@ +class PaywallUtils { + const PaywallUtils._(); + + /// Builds a paywall URL from a template. + /// + /// - Supports both raw placeholders (`{localPart}`, `{domainName}`) + /// and URL-encoded placeholders (`%7BlocalPart%7D`, `%7BdomainName%7D`). + /// - If [localPart] or [domainName] is not provided, the placeholder + /// is removed. + static String buildPaywallUrlFromTemplate({ + required String template, + String? localPart, + String? domainName, + }) { + final replacements = { + '{localPart}': localPart ?? '', + '{domainName}': domainName ?? '', + Uri.encodeComponent('{localPart}'): localPart ?? '', + Uri.encodeComponent('{domainName}'): domainName ?? '', + }; + + var result = template; + replacements.forEach((placeholder, value) { + result = result.replaceAll(placeholder, value); + }); + + return result; + } +} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 1a2dc8ec6..483c978de 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2025-09-09T13:40:24.145761", + "@@last_modified": "2025-09-09T15:28:33.596868", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -4687,5 +4687,11 @@ "placeholders": { "count": {} } + }, + "paywallUrlNotAvailable": "Paywall url not available", + "@paywallUrlNotAvailable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index b7854653b..ea466574d 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4943,4 +4943,11 @@ class AppLocalizations { args: [count], ); } + + String get paywallUrlNotAvailable { + return Intl.message( + 'Paywall url not available', + name: 'paywallUrlNotAvailable', + ); + } } diff --git a/model/lib/extensions/session_extension.dart b/model/lib/extensions/session_extension.dart index e035e777e..ca6fc441f 100644 --- a/model/lib/extensions/session_extension.dart +++ b/model/lib/extensions/session_extension.dart @@ -22,8 +22,6 @@ import 'package:model/principals/capability_principals.dart'; import 'package:uri/uri.dart'; extension SessionExtension on Session { - static const String saaSCapabilityProperties = 'com:linagora:params:saas'; - String getDownloadUrl({String? jmapUrl}) { final Uri downloadUrlValid; if (jmapUrl != null) { diff --git a/test/features/paywall/build_paywall_url_test.dart b/test/features/paywall/build_paywall_url_test.dart new file mode 100644 index 000000000..c11333da8 --- /dev/null +++ b/test/features/paywall/build_paywall_url_test.dart @@ -0,0 +1,170 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/paywall/presentation/paywall_utils.dart'; + +void main() { + group('PaywallUtils.buildPaywallUrlFromTemplate', () { + // --- Raw placeholder cases --- + test('replaces raw {localPart} and {domainName}', () { + const template = 'https://{localPart}.{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'alice', + domainName: 'example.com', + ); + expect(url, 'https://alice.example.com/paywall'); + }); + + test('removes {localPart} when null', () { + const template = 'https://{localPart}.{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + domainName: 'example.com', + ); + expect(url, 'https://.example.com/paywall'); + }); + + test('removes {domainName} when null', () { + const template = 'https://{localPart}.{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'bob', + ); + expect(url, 'https://bob./paywall'); + }); + + test('removes both {localPart} and {domainName} when both null', () { + const template = 'https://{localPart}.{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate(template: template); + expect(url, 'https://./paywall'); + }); + + // --- Encoded placeholder cases --- + test('replaces encoded %7BlocalPart%7D', () { + const template = 'https://%7BlocalPart%7D.twake.app/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'charlie', + ); + expect(url, 'https://charlie.twake.app/paywall'); + }); + + test('replaces encoded %7BdomainName%7D', () { + const template = 'https://account.%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + domainName: 'test.org', + ); + expect(url, 'https://account.test.org/paywall'); + }); + + test('replaces both encoded placeholders', () { + const template = 'https://%7BlocalPart%7D.%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'david', + domainName: 'mysite.com', + ); + expect(url, 'https://david.mysite.com/paywall'); + }); + + test('removes encoded placeholders when null', () { + const template = 'https://%7BlocalPart%7D.%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate(template: template); + expect(url, 'https://./paywall'); + }); + + // --- Mixed raw + encoded --- + test('handles raw {localPart} and encoded %7BdomainName%7D together', () { + const template = 'https://{localPart}.%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'eve', + domainName: 'hybrid.com', + ); + expect(url, 'https://eve.hybrid.com/paywall'); + }); + + test('handles encoded %7BlocalPart%7D and raw {domainName} together', () { + const template = 'https://%7BlocalPart%7D.{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'frank', + domainName: 'hybrid.org', + ); + expect(url, 'https://frank.hybrid.org/paywall'); + }); + + // --- No placeholders --- + test('keeps template unchanged if no placeholders', () { + const template = 'https://static.twake.app/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'ghost', + domainName: 'ignored.com', + ); + expect(url, 'https://static.twake.app/paywall'); + }); + + // --- Edge cases --- + test('empty template returns empty string', () { + const template = ''; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'x', + domainName: 'y.com', + ); + expect(url, ''); + }); + + test('template with only {localPart}', () { + const template = '{localPart}'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'single', + ); + expect(url, 'single'); + }); + + test('template with only {domainName}', () { + const template = '{domainName}'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + domainName: 'onedomain.com', + ); + expect(url, 'onedomain.com'); + }); + + test('template with repeated placeholders', () { + const template = + 'https://{localPart}.{domainName}/{localPart}-{domainName}/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'anna', + domainName: 'repeat.com', + ); + expect(url, 'https://anna.repeat.com/anna-repeat.com/paywall'); + }); + + test('template with repeated encoded placeholders', () { + const template = + 'https://%7BlocalPart%7D.%7BdomainName%7D/%7BlocalPart%7D-%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'zoe', + domainName: 'repeat.org', + ); + expect(url, 'https://zoe.repeat.org/zoe-repeat.org/paywall'); + }); + + test('template with mix of raw and encoded repeated placeholders', () { + const template = + 'https://{localPart}.%7BdomainName%7D/{localPart}-%7BdomainName%7D/paywall'; + final url = PaywallUtils.buildPaywallUrlFromTemplate( + template: template, + localPart: 'mix', + domainName: 'combo.net', + ); + expect(url, 'https://mix.combo.net/mix-combo.net/paywall'); + }); + }); +}