Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: firebase_deep_link_client
name: app_links_deep_link_client

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -7,8 +7,8 @@ concurrency:
on:
pull_request:
paths:
- "flutter_news_example/packages/deep_link_client/firebase_deep_link_client/**"
- ".github/workflows/firebase_deep_link_client.yaml"
- "flutter_news_example/packages/deep_link_client/app_links_deep_link_client/**"
- ".github/workflows/app_links_deep_link_client.yaml"
branches:
- main

Expand All @@ -17,4 +17,4 @@ jobs:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
flutter_version: 3.38.3
working_directory: flutter_news_example/packages/deep_link_client/firebase_deep_link_client
working_directory: flutter_news_example/packages/deep_link_client/app_links_deep_link_client
2 changes: 1 addition & 1 deletion docs/docs/flutter_development/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The package depends on the third-party packages that expose authentication metho

To enable authentication, configure each authentication method:

- For email login, enable the **Email/password sign-in provider** in the Firebase Console of your project. In the same section, enable the **Email link sign-in method**. On the dynamic links page, set up a new dynamic link URL prefix (for example, `yourApplicationName.page.link`) with a dynamic link URL of "/email_login".
- For email login, enable the **Email/password sign-in provider** in the Firebase Console of your project. In the same section, enable the **Email link sign-in method**. You will also need to configure deep links for your app - see the [deep link configuration](/project_configuration/social_authentication#deep-link-configuration) section for details.
- For Google login, enable the **Google sign-in provider** in the Firebase Console of your project. You might need to generate a `SHA1` key for use with Android.
- For Apple login, [configure sign-in with Apple](https://firebase.google.com/docs/auth/ios/apple#configure-sign-in-with-apple) in the Apple's developer portal and [enable the **Apple sign-in provider**](https://firebase.google.com/docs/auth/ios/apple#enable-apple-as-a-sign-in-provider) in the Firebase Console of your project.
- For Twitter login, register an app in the Twitter developer portal and enable the **Twitter sign-in provider** in the Firebase Console of your project.
Expand Down
18 changes: 15 additions & 3 deletions docs/docs/project_configuration/social_authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,23 @@ Passwordless authentication with an email link requires additional configuration

:::

Once the email authentication method is set up, go to **Firebase -> Engage -> Dynamic links**. Set up a new dynamic link URL prefix (for example, yourApplicationName.page.link) with a dynamic link URL of "/email_login".
### Deep link configuration

Once the dynamic link is set up, replace the placeholder value for **FLAVOR_DEEP_LINK_DOMAIN** inside the `launch.json` file with the **dynamic link URL prefix** you just created. This enviroment variable will be used inside `firebase_authentication_client.dart` to generate the dynamic link URL that will be sent to the user.
The toolkit uses [App Links](https://developer.android.com/training/app-links) (Android) and [Universal Links](https://developer.apple.com/ios/universal-links/) (iOS) for deep linking. To set up deep links for your app:

In addition, replace the placeholder value for every **FLAVOR_DEEP_LINK_DOMAIN** key within your `project.pbxproj` file with the dynamic link URL prefix you just created.
1. **Choose a domain you control** (e.g., `links.yourapp.com`) where you can host verification files.

2. **Host the verification files** on your domain:
- **Android**: Host `/.well-known/assetlinks.json` - see [Android App Links documentation](https://developer.android.com/training/app-links/verify-android-applinks)
- **iOS**: Host `/.well-known/apple-app-site-association` - see [Universal Links documentation](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)

3. **Update the configuration** by replacing the placeholder value `your-domain.com` for **FLAVOR_DEEP_LINK_DOMAIN** in the following files:
- `launch.json` (VS Code)
- `.idea/runConfigurations/development.xml` and `production.xml` (IntelliJ/Android Studio)
- `android/app/src/development/res/values/strings.xml`
- `ios/Runner.xcodeproj/project.pbxproj`

For more details on setting up deep links in Flutter, see the [Flutter deep linking documentation](https://docs.flutter.dev/ui/navigation/deep-linking).

## Google

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flutter_news_example/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"--target",
"lib/main/main_development.dart",
"--dart-define",
"FLAVOR_DEEP_LINK_DOMAIN=flutternewsexampledev.page.link",
"FLAVOR_DEEP_LINK_DOMAIN=your-domain.com",
"--dart-define",
"FLAVOR_DEEP_LINK_PATH=email_login",
"--dart-define",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<string name="facebook_app_id">3068290313391198</string>
<string name="facebook_client_token">7a51baf0a329829a55e6ab8ced3da73a</string>
<string name="twitter_redirect_uri_scheme">google-news-template</string>
<string name="flavor_deep_link_domain">googlenewstemplatedev.page.link</string>
<string name="flavor_deep_link_domain">your-domain.com</string>
<string name="admob_app_id">ca-app-pub-3940256099942544~3347511713</string>
</resources>
4 changes: 2 additions & 2 deletions flutter_news_example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@
FACEBOOK_CLIENT_TOKEN = 7a51baf0a329829a55e6ab8ced3da73a;
FACEBOOK_DISPLAY_NAME = "Flutter News Example";
FLAVOR_APP_NAME = "Flutter News Example [DEV]";
FLAVOR_DEEP_LINK_DOMAIN = googlenewstemplatedev.page.link;
FLAVOR_DEEP_LINK_DOMAIN = your-domain.com;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
Expand Down Expand Up @@ -737,7 +737,7 @@
FACEBOOK_CLIENT_TOKEN = 7a51baf0a329829a55e6ab8ced3da73a;
FACEBOOK_DISPLAY_NAME = "Flutter News Example";
FLAVOR_APP_NAME = "Flutter News Example [DEV]";
FLAVOR_DEEP_LINK_DOMAIN = googlenewstemplatedev.page.link;
FLAVOR_DEEP_LINK_DOMAIN = your-domain.com;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
Expand Down
77 changes: 37 additions & 40 deletions flutter_news_example/lib/main/bootstrap/bootstrap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'package:analytics_repository/analytics_repository.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_dynamic_links/firebase_dynamic_links.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
Expand All @@ -16,49 +15,47 @@ import 'package:shared_preferences/shared_preferences.dart';

typedef AppBuilder =
Future<Widget> Function(
// Ignore, as we currently only use dynamic links for e-mail login.
// e-mail login will be replaced but not deprecated.
// source:https://firebase.google.com/support/dynamic-links-faq#im_currently_using_or_need_to_use_dynamic_links_for_email_link_authentication_in_firebase_authentication_will_this_feature_continue_to_work_after_the_sunset
// ignore: deprecated_member_use
FirebaseDynamicLinks firebaseDynamicLinks,
FirebaseMessaging firebaseMessaging,
SharedPreferences sharedPreferences,
AnalyticsRepository analyticsRepository,
);

Future<void> bootstrap(AppBuilder builder) async {
await runZonedGuarded<Future<void>>(() async {
WidgetsFlutterBinding.ensureInitialized();

await Firebase.initializeApp();
final analyticsRepository = AnalyticsRepository(FirebaseAnalytics.instance);
final blocObserver = AppBlocObserver(
analyticsRepository: analyticsRepository,
);
Bloc.observer = blocObserver;
final storageDirectory = await getApplicationSupportDirectory();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(storageDirectory.path),
);

if (kDebugMode) {
await HydratedBloc.storage.clear();
}

await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

final sharedPreferences = await SharedPreferences.getInstance();

unawaited(MobileAds.instance.initialize());
runApp(
await builder(
// ignore: deprecated_member_use
FirebaseDynamicLinks.instance,
FirebaseMessaging.instance,
sharedPreferences,
analyticsRepository,
),
);
}, (_, _) {});
await runZonedGuarded<Future<void>>(
() async {
WidgetsFlutterBinding.ensureInitialized();

await Firebase.initializeApp();
final analyticsRepository = AnalyticsRepository(
FirebaseAnalytics.instance,
);
final blocObserver = AppBlocObserver(
analyticsRepository: analyticsRepository,
);
Bloc.observer = blocObserver;
final storageDirectory = await getApplicationSupportDirectory();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: HydratedStorageDirectory(storageDirectory.path),
);

if (kDebugMode) {
await HydratedBloc.storage.clear();
}

await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;

final sharedPreferences = await SharedPreferences.getInstance();

unawaited(MobileAds.instance.initialize());
runApp(
await builder(
FirebaseMessaging.instance,
sharedPreferences,
analyticsRepository,
),
);
},
(_, st) {},
);
}
7 changes: 2 additions & 5 deletions flutter_news_example/lib/main/main_development.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:ads_consent_client/ads_consent_client.dart';
import 'package:app_links_deep_link_client/app_links_deep_link_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 All @@ -20,7 +20,6 @@ import 'package:user_repository/user_repository.dart';

void main() async {
await bootstrap((
firebaseDynamicLinks,
firebaseMessaging,
sharedPreferences,
analyticsRepository,
Expand All @@ -44,9 +43,7 @@ void main() async {
);

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

final userStorage = UserStorage(storage: persistentStorage);
Expand Down
7 changes: 2 additions & 5 deletions flutter_news_example/lib/main/main_production.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:ads_consent_client/ads_consent_client.dart';
import 'package:app_links_deep_link_client/app_links_deep_link_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 All @@ -20,7 +20,6 @@ import 'package:user_repository/user_repository.dart';

void main() async {
await bootstrap((
firebaseDynamicLinks,
firebaseMessaging,
sharedPreferences,
analyticsRepository,
Expand All @@ -44,9 +43,7 @@ void main() async {
);

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

final userStorage = UserStorage(storage: persistentStorage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/app_links_deep_link_client.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'dart:async';

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

/// {@template app_links_deep_link_client}
/// An AppLinks implementation of [DeepLinkClient].
/// {@endtemplate}
class AppLinksDeepLinkClient implements DeepLinkClient {
/// {@macro app_links_deep_link_client}
AppLinksDeepLinkClient({AppLinks? appLinks})
: _appLinks = appLinks ?? AppLinks();

final AppLinks _appLinks;

@override
Stream<Uri> get deepLinkStream => _appLinks.uriLinkStream;

@override
Future<Uri?> getInitialLink() => _appLinks.getInitialLink();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: app_links_deep_link_client
description: A Dart package which provides a deep link stream using app_links
publish_to: none

environment:
sdk: ">=3.9.2 <4.0.0"
flutter: ">=3.10.0"

dependencies:
app_links: ^6.3.3
deep_link_client:
path: ../deep_link_client
flutter:
sdk: flutter

dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^1.0.2
very_good_analysis: ^10.0.0

Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:app_links_deep_link_client/app_links_deep_link_client.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockAppLinks extends Mock implements AppLinks {}

void main() {
late MockAppLinks appLinks;
late StreamController<Uri> uriLinkStreamController;

setUp(() {
appLinks = MockAppLinks();
uriLinkStreamController = StreamController<Uri>();
when(
() => appLinks.uriLinkStream,
).thenAnswer((_) => uriLinkStreamController.stream);
});

tearDown(() {
unawaited(uriLinkStreamController.close());
});

group('AppLinksDeepLinkClient', () {
test('can be instantiated without parameters', () {
expect(AppLinksDeepLinkClient.new, returnsNormally);
});
group('getInitialLink', () {
test('retrieves the latest link if present', () async {
final expectedUri = Uri.https('ham.app.test', '/test/path');
when(appLinks.getInitialLink).thenAnswer(
(_) => Future.value(expectedUri),
);

final client = AppLinksDeepLinkClient(
appLinks: appLinks,
);
final link = await client.getInitialLink();
expect(link, expectedUri);
});
});

group('deepLinkStream', () {
test('publishes values received through uriLinkStream', () {
final expectedUri1 = Uri.https('news.app.test', '/test/1');
final expectedUri2 = Uri.https('news.app.test', '/test/2');

final client = AppLinksDeepLinkClient(
appLinks: appLinks,
);

uriLinkStreamController
..add(expectedUri1)
..add(expectedUri1)
..add(expectedUri2)
..add(expectedUri1);

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

This file was deleted.

Loading