diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 806f7b7..52ad577 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -27,6 +27,10 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
index 903def2..a7f8c72 100644
--- a/ios/Runner/Runner.entitlements
+++ b/ios/Runner/Runner.entitlements
@@ -4,5 +4,9 @@
aps-environment
development
+ com.apple.developer.associated-domains
+
+ applinks:sprk.so
+
diff --git a/lib/src/core/utils/share_urls.dart b/lib/src/core/utils/share_urls.dart
index af4c3a3..1536fc6 100644
--- a/lib/src/core/utils/share_urls.dart
+++ b/lib/src/core/utils/share_urls.dart
@@ -13,25 +13,99 @@ String normalizeSparkPostUri(String postUri) {
return normalizedPostUri;
}
+String? canonicalizeSparkPostUri(String postUri) {
+ final trimmedPostUri = postUri.trim();
+ if (trimmedPostUri.isEmpty) {
+ return null;
+ }
+
+ final canonicalMatch = _parseCanonicalSparkUri(trimmedPostUri);
+ if (canonicalMatch != null) {
+ return trimmedPostUri;
+ }
+
+ final shortUriMatch = RegExp(
+ r'^(did:[^/]+)/([^/?#]+)$',
+ ).firstMatch(normalizeSparkPostUri(trimmedPostUri));
+ if (shortUriMatch == null) {
+ return null;
+ }
+
+ final did = shortUriMatch.group(1)!;
+ final rkey = shortUriMatch.group(2)!;
+ return 'at://$did/so.sprk.feed.post/$rkey';
+}
+
+({String did, String rkey})? _parseCanonicalSparkUri(String uri) {
+ final match = RegExp(
+ r'^at://([^/]+)/so\.sprk\.feed\.post/([^/?#]+)$',
+ ).firstMatch(uri);
+ if (match == null) return null;
+ return (did: match.group(1)!, rkey: match.group(2)!);
+}
+
String buildSparkShareUrl(String postUri) {
final normalizedPostUri = normalizeSparkPostUri(postUri);
+ final parts = _parseCanonicalSparkUri(normalizedPostUri);
+ if (parts == null) {
+ return Uri.https(_sparkShareHost, '/watch', {
+ 'uri': normalizedPostUri,
+ }).toString();
+ }
- return Uri.https(_sparkShareHost, '/watch', {
- 'uri': normalizedPostUri,
- }).toString();
+ return Uri.https(
+ _sparkShareHost,
+ '/post/${parts.did}/${parts.rkey}',
+ ).toString();
}
String? extractSparkPostUri(String url) {
+ final canonicalPostUri = extractCanonicalSparkPostUri(url);
+ if (canonicalPostUri == null) {
+ return null;
+ }
+
+ return normalizeSparkPostUri(canonicalPostUri);
+}
+
+String? extractCanonicalSparkPostUri(String url) {
try {
final uri = Uri.parse(url);
- final postUri = uri.queryParameters['uri'];
- final isSparkHost = uri.host == _sparkShareHost && uri.path == '/watch';
+ final isSparkHost = uri.host == _sparkShareHost;
final isLegacySparkHost = uri.host == _legacySparkShareHost;
- if ((isSparkHost || isLegacySparkHost) &&
- postUri != null &&
- postUri.isNotEmpty) {
- return normalizeSparkPostUri(postUri);
+ if (isSparkHost || isLegacySparkHost) {
+ final postPathMatch = RegExp(
+ r'^/post/([^/]+)/([^/?#]+)$',
+ ).firstMatch(uri.path);
+ if (postPathMatch != null) {
+ final identifier = postPathMatch.group(1)!;
+ final rkey = postPathMatch.group(2)!;
+ final did = identifier.startsWith('did:')
+ ? identifier
+ : 'did:plc:$identifier';
+ return 'at://$did/so.sprk.feed.post/$rkey';
+ }
+
+ final postUri = uri.queryParameters['uri'];
+ if ((uri.path == '/watch' || isLegacySparkHost) &&
+ postUri != null &&
+ postUri.isNotEmpty) {
+ return canonicalizeSparkPostUri(postUri);
+ }
+
+ final shortDid = uri.queryParameters['u'];
+ final rkey = uri.queryParameters['p'];
+ if ((uri.path == '/watch' || isLegacySparkHost) &&
+ shortDid != null &&
+ shortDid.isNotEmpty &&
+ rkey != null &&
+ rkey.isNotEmpty) {
+ final did = shortDid.startsWith('did:')
+ ? shortDid
+ : 'did:plc:$shortDid';
+ return 'at://$did/so.sprk.feed.post/$rkey';
+ }
}
} catch (_) {
// Ignore malformed URLs.
diff --git a/lib/src/sprk_app.dart b/lib/src/sprk_app.dart
index 657f06c..036f95e 100644
--- a/lib/src/sprk_app.dart
+++ b/lib/src/sprk_app.dart
@@ -6,6 +6,7 @@ import 'package:spark/src/core/design_system/theme/app_theme.dart';
import 'package:spark/src/core/l10n/app_localizations.dart';
import 'package:spark/src/core/routing/app_router.dart';
import 'package:spark/src/core/ui/theme/providers/theme_provider.dart';
+import 'package:spark/src/core/utils/share_urls.dart';
import 'package:spark/src/core/utils/logging/log_service.dart';
import 'package:spark/src/core/utils/logging/logger.dart';
import 'package:spark/src/features/feed/providers/feed_provider.dart';
@@ -89,7 +90,22 @@ class _SprkAppState extends ConsumerState {
}
return supportedLocales.first;
},
- routerConfig: _appRouter.config(),
+ routerConfig: _appRouter.config(
+ deepLinkTransformer: _transformIncomingDeepLink,
+ ),
+ );
+ }
+
+ Future _transformIncomingDeepLink(Uri uri) async {
+ final canonicalPostUri = extractCanonicalSparkPostUri(uri.toString());
+ if (canonicalPostUri == null) {
+ return uri;
+ }
+
+ final transformedUri = Uri(
+ path: '/post/${Uri.encodeComponent(canonicalPostUri)}',
);
+ _logger.i('Transformed incoming deep link from $uri to $transformedUri');
+ return transformedUri;
}
}