TF-4145 Use generateWebLink for paywall url

This commit is contained in:
dab246
2025-11-12 18:24:51 +07:00
committed by Dat H. Pham
parent 8b5fb2f030
commit 14ab5352a3
3 changed files with 96 additions and 41 deletions
+19 -17
View File
@@ -2,16 +2,14 @@ import 'package:core/core.dart';
/// Utility class for generating web links in flat subdomain format. /// Utility class for generating web links in flat subdomain format.
class WebLinkGenerator { class WebLinkGenerator {
const WebLinkGenerator();
/// Ensures that a path starts with `/`. /// Ensures that a path starts with `/`.
String ensureFirstSlash(String? path) { static String ensureFirstSlash(String? path) {
if (path == null || path.isEmpty) return '/'; if (path == null || path.isEmpty) return '/';
return path.startsWith('/') ? path : '/$path'; return path.startsWith('/') ? path : '/$path';
} }
/// Validates if a given FQDN looks valid (has at least 2 parts and non-empty). /// Validates if a given FQDN looks valid (has at least 2 parts and non-empty).
bool isValidFqdn(String fqdn) { static bool isValidFqdn(String fqdn) {
if (fqdn.trim().isEmpty || !fqdn.contains('.')) return false; if (fqdn.trim().isEmpty || !fqdn.contains('.')) return false;
final parts = fqdn.split('.').where((p) => p.trim().isNotEmpty).toList(); final parts = fqdn.split('.').where((p) => p.trim().isNotEmpty).toList();
return parts.length >= 2; return parts.length >= 2;
@@ -20,9 +18,9 @@ class WebLinkGenerator {
/// Generates a web link based on a workplace FQDN and app slug. /// Generates a web link based on a workplace FQDN and app slug.
/// ///
/// Throws [ArgumentError] when input is invalid. /// Throws [ArgumentError] when input is invalid.
String generateWebLink({ static String generateWebLink({
required String workplaceFqdn, required String workplaceFqdn,
required String slug, String? slug,
String? pathname, String? pathname,
String? hash, String? hash,
List<List<String>>? searchParams, List<List<String>>? searchParams,
@@ -31,16 +29,22 @@ class WebLinkGenerator {
throw ArgumentError('Invalid workplace FQDN: $workplaceFqdn'); throw ArgumentError('Invalid workplace FQDN: $workplaceFqdn');
} }
final hostParts = workplaceFqdn // Start from the base host
.split('.') String newHost = workplaceFqdn;
.where((p) => p.trim().isNotEmpty)
.toList();
hostParts[0] = '${hostParts[0]}-$slug'; // Only modify the host when slug is non-null and not empty
final newHost = hostParts.join('.'); if (slug != null && slug.trim().isNotEmpty) {
final hostParts =
workplaceFqdn.split('.').where((p) => p.trim().isNotEmpty).toList();
// Insert slug as a prefix to the first part (flat subdomain)
hostParts[0] = '${hostParts[0]}-$slug';
newHost = hostParts.join('.');
}
final safePath = ensureFirstSlash(pathname); final safePath = ensureFirstSlash(pathname);
final safeHash = (hash == null || hash.isEmpty) ? null : ensureFirstSlash(hash); final safeHash =
(hash == null || hash.isEmpty) ? null : ensureFirstSlash(hash);
final queryParams = <String, String>{}; final queryParams = <String, String>{};
for (final param in searchParams ?? const []) { for (final param in searchParams ?? const []) {
@@ -61,16 +65,14 @@ class WebLinkGenerator {
/// A safe version of [generateWebLink] that **never throws**. /// A safe version of [generateWebLink] that **never throws**.
/// ///
/// Returns `''` (empty string) when an error occurs. /// Returns `''` (empty string) when an error occurs.
String safeGenerateWebLink({ static String safeGenerateWebLink({
required String workplaceFqdn, required String workplaceFqdn,
required String slug, String? slug,
String? pathname, String? pathname,
String? hash, String? hash,
List<List<String>>? searchParams, List<List<String>>? searchParams,
}) { }) {
try { try {
if (slug.trim().isEmpty) return '';
return generateWebLink( return generateWebLink(
workplaceFqdn: workplaceFqdn, workplaceFqdn: workplaceFqdn,
slug: slug, slug: slug,
+52 -18
View File
@@ -2,11 +2,9 @@ import 'package:core/utils/web_link_generator.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
group('WebLinkGenerator (with workplaceFqdn)', () { group('WebLinkGenerator', () {
const generator = WebLinkGenerator();
test('should generate correct link for flat subdomain', () { test('should generate correct link for flat subdomain', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'alice.example.app', workplaceFqdn: 'alice.example.app',
searchParams: [ searchParams: [
['sharecode', 'sharingIsCaring'], ['sharecode', 'sharingIsCaring'],
@@ -24,7 +22,7 @@ void main() {
}); });
test('should prepend slash to path and hash if missing', () { test('should prepend slash to path and hash if missing', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'bob.example.tools', workplaceFqdn: 'bob.example.tools',
pathname: 'files', pathname: 'files',
hash: 'folder/123', hash: 'folder/123',
@@ -35,7 +33,7 @@ void main() {
}); });
test('should work with no search params', () { test('should work with no search params', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'charlie.domain.com', workplaceFqdn: 'charlie.domain.com',
slug: 'settings', slug: 'settings',
); );
@@ -43,9 +41,9 @@ void main() {
expect(result, 'https://charlie-settings.domain.com/'); expect(result, 'https://charlie-settings.domain.com/');
}); });
test('should throw when workplaceFqdn is empty', () { test('should throw when workplaceFqdn is empty', () {
expect( expect(
() => generator.generateWebLink( () => WebLinkGenerator.generateWebLink(
workplaceFqdn: '', workplaceFqdn: '',
slug: 'notes', slug: 'notes',
), ),
@@ -55,7 +53,7 @@ void main() {
test('should throw when workplaceFqdn only contains TLD', () { test('should throw when workplaceFqdn only contains TLD', () {
expect( expect(
() => generator.generateWebLink( () => WebLinkGenerator.generateWebLink(
workplaceFqdn: 'com', workplaceFqdn: 'com',
slug: 'notes', slug: 'notes',
), ),
@@ -65,7 +63,7 @@ void main() {
test('should throw when workplaceFqdn has no dot', () { test('should throw when workplaceFqdn has no dot', () {
expect( expect(
() => generator.generateWebLink( () => WebLinkGenerator.generateWebLink(
workplaceFqdn: 'localhost', workplaceFqdn: 'localhost',
slug: 'notes', slug: 'notes',
), ),
@@ -74,7 +72,7 @@ void main() {
}); });
test('should handle malformed FQDN gracefully (multiple dots)', () { test('should handle malformed FQDN gracefully (multiple dots)', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: '....example..app', workplaceFqdn: '....example..app',
slug: 'notes', slug: 'notes',
); );
@@ -84,7 +82,7 @@ void main() {
}); });
test('should ignore malformed searchParams', () { test('should ignore malformed searchParams', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'dave.example.app', workplaceFqdn: 'dave.example.app',
slug: 'drive', slug: 'drive',
searchParams: [ searchParams: [
@@ -97,7 +95,7 @@ void main() {
}); });
test('should not add fragment when hash is empty string', () { test('should not add fragment when hash is empty string', () {
final result = generator.generateWebLink( final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'eve.example.app', workplaceFqdn: 'eve.example.app',
slug: 'settings', slug: 'settings',
hash: '', hash: '',
@@ -106,8 +104,35 @@ void main() {
expect(result, 'https://eve-settings.example.app/'); expect(result, 'https://eve-settings.example.app/');
}); });
test('should keep original host when slug is null', () {
final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'alice.example.app',
slug: null,
);
expect(result, 'https://alice.example.app/');
});
test('should keep original host when slug is empty', () {
final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'alice.example.app',
slug: '',
);
expect(result, 'https://alice.example.app/');
});
test('should keep original host when slug is only whitespace', () {
final result = WebLinkGenerator.generateWebLink(
workplaceFqdn: 'bob.example.app',
slug: ' ',
);
expect(result, 'https://bob.example.app/');
});
test('safeGenerateWebLink should return empty string on invalid FQDN', () { test('safeGenerateWebLink should return empty string on invalid FQDN', () {
final result = generator.safeGenerateWebLink( final result = WebLinkGenerator.safeGenerateWebLink(
workplaceFqdn: '', workplaceFqdn: '',
slug: 'notes', slug: 'notes',
); );
@@ -115,22 +140,31 @@ void main() {
expect(result, ''); expect(result, '');
}); });
test('safeGenerateWebLink should return empty string on missing slug', () { test('safeGenerateWebLink should return valid URL on missing slug', () {
final result = generator.safeGenerateWebLink( final result = WebLinkGenerator.safeGenerateWebLink(
workplaceFqdn: 'alice.example.app', workplaceFqdn: 'alice.example.app',
slug: '', slug: '',
); );
expect(result, ''); expect(result, 'https://alice.example.app/');
}); });
test('safeGenerateWebLink should still return valid URL when input ok', () { test('safeGenerateWebLink should still return valid URL when input ok', () {
final result = generator.safeGenerateWebLink( final result = WebLinkGenerator.safeGenerateWebLink(
workplaceFqdn: 'bob.example.app', workplaceFqdn: 'bob.example.app',
slug: 'drive', slug: 'drive',
); );
expect(result, 'https://bob-drive.example.app/'); expect(result, 'https://bob-drive.example.app/');
}); });
test('safeGenerateWebLink should handle slug = null gracefully', () {
final result = WebLinkGenerator.safeGenerateWebLink(
workplaceFqdn: 'example.com',
slug: null,
);
expect(result, 'https://example.com/');
});
}); });
} }
@@ -1,6 +1,8 @@
import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/failure.dart';
import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/state/success.dart';
import 'package:core/utils/app_logger.dart';
import 'package:core/utils/web_link_generator.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart';
@@ -39,10 +41,7 @@ class PaywallController extends BaseController {
paywallUrlPattern = newPattern; paywallUrlPattern = newPattern;
if (isRetryGetPaywallUrl) { if (isRetryGetPaywallUrl) {
isRetryGetPaywallUrl = false; isRetryGetPaywallUrl = false;
final qualifiedPaywall = paywallUrlPattern!.getQualifiedUrl( final qualifiedPaywall = getPaywallUrl(paywallUrlPattern!);
ownerEmail: ownEmailAddress,
domainName: RouteUtils.getRootDomain(),
);
AppUtils.launchLink(qualifiedPaywall); AppUtils.launchLink(qualifiedPaywall);
} }
} }
@@ -88,13 +87,33 @@ class PaywallController extends BaseController {
_showMessagePaywallUrlNotAvailable(context: context); _showMessagePaywallUrlNotAvailable(context: context);
return; return;
} }
final qualifiedPaywall = getPaywallUrl(paywallUrlPattern!);
AppUtils.launchLink(qualifiedPaywall);
}
final qualifiedPaywall = paywallUrlPattern!.getQualifiedUrl( String getPaywallUrl(PaywallUrlPattern pattern) {
final defaultUrl = pattern.getQualifiedUrl(
ownerEmail: ownEmailAddress, ownerEmail: ownEmailAddress,
domainName: RouteUtils.getRootDomain(), domainName: RouteUtils.getRootDomain(),
); );
AppUtils.launchLink(qualifiedPaywall); try {
final workplaceFqdn = twakeAppManager.oidcUserInfo?.workplaceFqdn?.trim();
if (workplaceFqdn == null || workplaceFqdn.isEmpty) {
return defaultUrl;
}
final paywallUrl = WebLinkGenerator.safeGenerateWebLink(
workplaceFqdn: workplaceFqdn,
pathname: 'paywall',
);
// Fallback if generated URL is empty or invalid
return paywallUrl.isNotEmpty ? paywallUrl : defaultUrl;
} catch (e) {
logError('$runtimeType::getPaywallUrl: Failed to generate paywall URL: $e');
return defaultUrl;
}
} }
void _handleRetryGetPaywallUrl() { void _handleRetryGetPaywallUrl() {