Skip to content

Commit

Permalink
feat: create abstract layer for deep_link_client to simplify other im…
Browse files Browse the repository at this point in the history
…plementations (#1158)
  • Loading branch information
elianortega authored Sep 26, 2024
1 parent 5354d19 commit ee9bc1b
Show file tree
Hide file tree
Showing 22 changed files with 298 additions and 185 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/deep_link_client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ concurrency:
on:
pull_request:
paths:
- "flutter_news_example/packages/deep_link_client/**"
- "flutter_news_example/packages/deep_link_client/deep_link_client/**"
- ".github/workflows/deep_link_client.yaml"
branches:
- main

jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/dart_package.yml@v1
with:
flutter_version: 3.24.2
working_directory: flutter_news_example/packages/deep_link_client
dart_sdk: 3.4.3
working_directory: flutter_news_example/packages/deep_link_client/deep_link_client
20 changes: 20 additions & 0 deletions .github/workflows/firebase_deep_link_client.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: firebase_deep_link_client

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
pull_request:
paths:
- "flutter_news_example/packages/deep_link_client/firebase_deep_link_client/**"
- ".github/workflows/firebase_deep_link_client.yaml"
branches:
- main

jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
flutter_version: 3.22.2
working_directory: flutter_news_example/packages/deep_link_client/firebase_deep_link_client
9 changes: 6 additions & 3 deletions flutter_news_example/lib/main/main_development.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:ads_consent_client/ads_consent_client.dart';
import 'package:article_repository/article_repository.dart';
import 'package:deep_link_client/deep_link_client.dart';
import 'package:firebase_authentication_client/firebase_authentication_client.dart';
import 'package:firebase_deep_link_client/firebase_deep_link_client.dart';
import 'package:firebase_notifications_client/firebase_notifications_client.dart';
import 'package:flutter_news_example/app/app.dart';
import 'package:flutter_news_example/main/bootstrap/bootstrap.dart';
Expand Down Expand Up @@ -43,8 +44,10 @@ void main() {
packageVersion: packageVersion,
);

final deepLinkClient = DeepLinkClient(
firebaseDynamicLinks: firebaseDynamicLinks,
final deepLinkService = DeepLinkService(
deepLinkClient: FirebaseDeepLinkClient(
firebaseDynamicLinks: firebaseDynamicLinks,
),
);

final userStorage = UserStorage(storage: persistentStorage);
Expand All @@ -61,7 +64,7 @@ void main() {
apiClient: apiClient,
authenticationClient: authenticationClient,
packageInfoClient: packageInfoClient,
deepLinkClient: deepLinkClient,
deepLinkService: deepLinkService,
storage: userStorage,
);

Expand Down
9 changes: 6 additions & 3 deletions flutter_news_example/lib/main/main_production.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:ads_consent_client/ads_consent_client.dart';
import 'package:article_repository/article_repository.dart';
import 'package:deep_link_client/deep_link_client.dart';
import 'package:firebase_authentication_client/firebase_authentication_client.dart';
import 'package:firebase_deep_link_client/firebase_deep_link_client.dart';
import 'package:firebase_notifications_client/firebase_notifications_client.dart';
import 'package:flutter_news_example/app/app.dart';
import 'package:flutter_news_example/main/bootstrap/bootstrap.dart';
Expand Down Expand Up @@ -43,8 +44,10 @@ void main() {
packageVersion: packageVersion,
);

final deepLinkClient = DeepLinkClient(
firebaseDynamicLinks: firebaseDynamicLinks,
final deepLinkService = DeepLinkService(
deepLinkClient: FirebaseDeepLinkClient(
firebaseDynamicLinks: firebaseDynamicLinks,
),
);

final userStorage = UserStorage(storage: persistentStorage);
Expand All @@ -61,7 +64,7 @@ void main() {
apiClient: apiClient,
authenticationClient: authenticationClient,
packageInfoClient: packageInfoClient,
deepLinkClient: deepLinkClient,
deepLinkService: deepLinkService,
storage: userStorage,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.1.0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'src/deep_link_client.dart';
export 'src/deep_link_service.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// {@template deep_link_client}
/// A generic DeepLinkClient interface.
/// {@endtemplate}
abstract class DeepLinkClient {
/// Provides a stream of URIs intercepted by the app. Will emit the latest
/// received value (if any) as first.
Stream<Uri> get deepLinkStream;

/// Retrieves the initial deep link if present.
Future<Uri?> getInitialLink();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import 'dart:async';

import 'package:deep_link_client/deep_link_client.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';

import 'package:rxdart/rxdart.dart';

/// {@template deep_link_client_failure}
Expand All @@ -21,21 +20,20 @@ class DeepLinkClientFailure with EquatableMixin implements Exception {
List<Object> get props => [error];
}

/// {@template deep_link_client}
/// A client that exposes a stream of deep link URIs redirected to the app.
/// {@template deep_link_service}
/// A DeepLinkService that provides access to deep links intercepted by the app.
/// {@endtemplate}
class DeepLinkClient {
/// {@macro deep_link_client}
DeepLinkClient({FirebaseDynamicLinks? firebaseDynamicLinks})
: _deepLinkSubject = BehaviorSubject<Uri>() {
_firebaseDynamicLinks =
firebaseDynamicLinks ?? FirebaseDynamicLinks.instance;

class DeepLinkService {
/// {@macro deep_link_service}
DeepLinkService({
required DeepLinkClient deepLinkClient,
}) : _deepLinkClient = deepLinkClient,
_deepLinkSubject = BehaviorSubject<Uri>() {
unawaited(_getInitialLink());
_firebaseDynamicLinks.onLink.listen(_onAppLink).onError(_handleError);
_deepLinkClient.deepLinkStream.listen(_onAppLink).onError(_handleError);
}

late final FirebaseDynamicLinks _firebaseDynamicLinks;
final DeepLinkClient _deepLinkClient;
final BehaviorSubject<Uri> _deepLinkSubject;

/// Provides a stream of URIs intercepted by the app. Will emit the latest
Expand All @@ -44,7 +42,7 @@ class DeepLinkClient {

Future<void> _getInitialLink() async {
try {
final deepLink = await _firebaseDynamicLinks.getInitialLink();
final deepLink = await _deepLinkClient.getInitialLink();
if (deepLink != null) {
_onAppLink(deepLink);
}
Expand All @@ -53,8 +51,8 @@ class DeepLinkClient {
}
}

void _onAppLink(PendingDynamicLinkData dynamicLinkData) {
_deepLinkSubject.add(dynamicLinkData.link);
void _onAppLink(Uri deepLink) {
_deepLinkSubject.add(deepLink);
}

void _handleError(Object error, StackTrace stackTrace) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: deep_link_client
description: A deep link client interface
publish_to: none

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
equatable: ^2.0.5
rxdart: ^0.27.5

dev_dependencies:
mocktail: ^1.0.4
test: ^1.25.8
very_good_analysis: ^6.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:async';

import 'package:deep_link_client/deep_link_client.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

typedef OnAppLinkFunction = void Function(Uri uri, String stringUri);

class MockDeepLinkClient extends Mock implements DeepLinkClient {}

void main() {
late DeepLinkClient deepLinkClient;
late StreamController<Uri> onDeepLinkStreamController;

setUp(() {
deepLinkClient = MockDeepLinkClient();
onDeepLinkStreamController = StreamController<Uri>();
when(() => deepLinkClient.deepLinkStream)
.thenAnswer((_) => onDeepLinkStreamController.stream);
});

tearDown(() {
onDeepLinkStreamController.close();
});

group('DeepLinkService', () {
test('retrieves and publishes latest link if present', () {
final expectedUri = Uri.https('ham.app.test', '/test/path');
when(deepLinkClient.getInitialLink).thenAnswer(
(_) => Future.value(expectedUri),
);

final service = DeepLinkService(deepLinkClient: deepLinkClient);
expect(service.deepLinkStream, emits(expectedUri));

// Testing also the replay of the latest value.
expect(service.deepLinkStream, emits(expectedUri));
});

test('publishes DeepLinkClientFailure to stream if upstream throws', () {
final expectedError = Error();
final expectedStackTrace = StackTrace.current;

when(deepLinkClient.getInitialLink).thenAnswer((_) {
return Future.error(expectedError, expectedStackTrace);
});

final deepLinkService = DeepLinkService(deepLinkClient: deepLinkClient);
expect(
deepLinkService.deepLinkStream,
emitsError(
isA<DeepLinkClientFailure>()
.having((failure) => failure.error, 'error', expectedError),
),
);
});

test('publishes values received through onAppLink callback', () {
final expectedUri1 = Uri.https('ham.app.test', '/test/1');
final expectedUri2 = Uri.https('ham.app.test', '/test/2');

when(deepLinkClient.getInitialLink).thenAnswer((_) async => null);

final deepLinkService = DeepLinkService(deepLinkClient: deepLinkClient);

expect(
deepLinkService.deepLinkStream,
emitsInOrder(
<Uri>[expectedUri1, expectedUri1, expectedUri2, expectedUri1],
),
);

onDeepLinkStreamController
..add(expectedUri1)
..add(expectedUri1)
..add(expectedUri2)
..add(expectedUri1);
});
});

group('DeepLinkClientFailure', () {
final error = Exception('errorMessage');

test('has correct props', () {
expect(
DeepLinkClientFailure(error).props,
[error],
);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/firebase_deep_link_client.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// ignore_for_file: deprecated_member_use

import 'dart:async';

import 'package:deep_link_client/deep_link_client.dart';
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';

/// {@template firebase_deep_link_client}
/// A FirebaseDynamicLinks implementation of [DeepLinkClient].
/// {@endtemplate}
class FirebaseDeepLinkClient implements DeepLinkClient {
/// {@macro firebase_deep_link_client}
FirebaseDeepLinkClient({required FirebaseDynamicLinks firebaseDynamicLinks})
: _firebaseDynamicLinks = firebaseDynamicLinks;

final FirebaseDynamicLinks _firebaseDynamicLinks;

@override
Stream<Uri> get deepLinkStream =>
_firebaseDynamicLinks.onLink.map((event) => event.link);

@override
Future<Uri?> getInitialLink() async {
final deepLink = await _firebaseDynamicLinks.getInitialLink();
return deepLink?.link;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
name: deep_link_client
name: firebase_deep_link_client
description: A Dart package which provides a deep link stream
publish_to: none

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
deep_link_client:
path: ../deep_link_client
equatable: ^2.0.3
firebase_core: ^3.4.1
firebase_dynamic_links: ^6.0.6
plugin_platform_interface: ^2.1.3
rxdart: ^0.27.3

dev_dependencies:
firebase_core_platform_interface: ^5.2.1
Expand Down
Loading

0 comments on commit ee9bc1b

Please sign in to comment.