TF-4145 Use generateWebLink for paywall url
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user