diff --git a/.github/workflows/firebase_dynamic_links.yaml b/.github/workflows/firebase_dynamic_links.yaml index 7b52969fd7bb..4ce0ce62b662 100644 --- a/.github/workflows/firebase_dynamic_links.yaml +++ b/.github/workflows/firebase_dynamic_links.yaml @@ -40,11 +40,10 @@ jobs: - name: "Drive Example" uses: reactivecircus/android-emulator-runner@v2 with: - api-level: 28 + api-level: 29 arch: x86_64 - # Firebase Database works without Google Play Services, so we don't use the `googleapis` - # emulator target as it's considerably slower on CI. - target: default + # Firebase Dynamic Links requires Google Play Services, so we use the `google_apis` + target: google_apis profile: Nexus 5X script: ./.github/workflows/scripts/drive-example.sh android diff --git a/docs/_assets/dl-apple-configure.jpg b/docs/_assets/dl-apple-configure.jpg new file mode 100644 index 000000000000..2c747c7ae410 Binary files /dev/null and b/docs/_assets/dl-apple-configure.jpg differ diff --git a/docs/_assets/dl-apple-provision.jpg b/docs/_assets/dl-apple-provision.jpg new file mode 100644 index 000000000000..f14ba82ec97e Binary files /dev/null and b/docs/_assets/dl-apple-provision.jpg differ diff --git a/docs/_assets/dl-apple-signing.jpg b/docs/_assets/dl-apple-signing.jpg new file mode 100644 index 000000000000..af693b3221fe Binary files /dev/null and b/docs/_assets/dl-apple-signing.jpg differ diff --git a/docs/_assets/dl-apple-urlscheme.jpg b/docs/_assets/dl-apple-urlscheme.jpg new file mode 100644 index 000000000000..579f151f0233 Binary files /dev/null and b/docs/_assets/dl-apple-urlscheme.jpg differ diff --git a/docs/_assets/dl-prefix.png b/docs/_assets/dl-prefix.png new file mode 100644 index 000000000000..1e84eb9b831c Binary files /dev/null and b/docs/_assets/dl-prefix.png differ diff --git a/docs/dynamic-links/android-integration.mdx b/docs/dynamic-links/android-integration.mdx new file mode 100644 index 000000000000..f90f7883723d --- /dev/null +++ b/docs/dynamic-links/android-integration.mdx @@ -0,0 +1,31 @@ +--- +title: Android Dynamic Links Setup +sidebar_label: Android Integration +description: Android requires additional configuration steps to be completed before you can receive dynamic links. +--- + +## Configure your App in the Firebase Console + +Create a SHA-256 fingerprint using these [instructions](https://developers.google.com/android/guides/client-auth) for your app, +and add to your app in your Firebase console. + +Next, go to the following location in your browser `[your-domain]/.well-known/assetlinks.json`. The response will have a target object +containing a "package_name" which ought to have your app's package name. Please do not proceed until you see this, it may take a while to register. + +## AndroidManifest.xml Configuration + +Add your deep link domains to your `android/app/src/main/AndroidManifest.xml` so your app can receive the Dynamic Link data after it is installed/updated +from the Play Store. Refer to the official docs to illustrate [setup](https://firebase.google.com/docs/dynamic-links/android/receive#add-an-intent-filter-for-deep-links). + +For example: + +```xml + + + + + + +``` diff --git a/docs/dynamic-links/apple-integration.mdx b/docs/dynamic-links/apple-integration.mdx new file mode 100644 index 000000000000..8fb433a68e9c --- /dev/null +++ b/docs/dynamic-links/apple-integration.mdx @@ -0,0 +1,66 @@ +--- +title: iOS Dynamic Links Setup +sidebar_label: Apple Integration +description: iOS requires additional configuration steps to be completed before you can receive dynamic links. +--- + +## Apple Account + +To setup Dynamic Links on iOS, it is a prerequisite that you have an Apple developer account [setup](https://developer.apple.com/programs/enroll/). + +## Configure your App in the Firebase Console + +Add an `App Store ID` & `Team ID` to your app in your Firebase console. If you do not have an `App Store ID` yet, you can put any number in here for now. +Your `Team ID` can be found in your Apple developer console. + +Apple Configuration + +Test the domain (e.g. `https://your-dynamic-link-domain`) you have created in your Firebase console. Go to the following location in your browser `[your domain]/apple-app-site-association`. +The response will have a details array property containing an object that has the property `appID`. That will be your app's app ID (It may take some time for +your domain to register). Please ensure it is registered before proceeding. + +## Apple Developer Console + +Create a provisioning profile for your app. Please ensure you've enabled the `Associated Domain` capability which you should check before proceeding. + +Apple Provisioning Profile + +## Signing & Capabilities + +Open your app under the `TARGETS` header using XCode. Click the `Signing & Capabilities` tab. You will need to ensure your `Team` is registered, and your `Provisioning Profile` field is completed. +Please add the domain you created in your Firebase console to the `Associated Domains` and prefix with `applinks:` + +Signing & Capabilities + +Click the `Info` tab, and add a `URL Type` to your project. The `Identifier` can be called `Bundle Id` or whatever you wish. Add your bundle identifier to the `URL Schemes` property. + +URL Schemes + +## Dynamic Links With Custom Domains + +If you have set up a custom domain for your Firebase project, you must add the dynamic link URL prefix into your iOS project's `Info.plist` file by using the `FirebaseDynamicLinksCustomDomains` key. +You can add multiple URLs as well. + +```xml + + + + + FirebaseDynamicLinksCustomDomains + + https://custom.domain.io/path1 + https://custom.domain.io/path2 + + + ...other settings + + + +``` + +If you don't add this, the dynamic link will invoke your app, but you cannot retrieve any deep link data you may need within your app, as the deep link will be completely ignored. + +## Test Dynamic Links + +To test your dynamic link, you will need to use a real device as it will not work on a simulator. You will also have to run the app in release mode (i.e. `flutter run --release`) as iOS will block you from opening +the app in debug mode from a dynamic link. diff --git a/docs/dynamic-links/overview.mdx b/docs/dynamic-links/overview.mdx new file mode 100644 index 000000000000..40ff7c3d48af --- /dev/null +++ b/docs/dynamic-links/overview.mdx @@ -0,0 +1,106 @@ +--- +title: Dynamic Links for Firebase +sidebar_label: Overview +--- + +## What does it do? + +Dynamic Links are links that work the way you want, on multiple platforms, and whether or not your app is already installed. +If a user opens a Dynamic Link on iOS or Android, they can be taken directly to the linked content in your native app. +If a user doesn't have your app installed, the user can be prompted to install it; then, after installation, your app starts +and can access the link. + + + +## Installation + + + + + + +Ensure you're using the Flutter `stable` channel: + +```bash +$ flutter channel stable +``` + +If your app is mixing legacy and null-safe packages, use the `--no-sound-null-safety` flag: +```bash +$ flutter run --no-sound-null-safety +``` + +For legacy package imports, place the following ignore comment to hide Dart analyzer warnings: + +```dart +// ignore: import_of_legacy_library_into_null_safe +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +``` + + + +### 1. Add dependency + + + + +```yaml {5} title="pubspec.yaml" +dependencies: + flutter: + sdk: flutter + firebase_core: "^{{ plugins.firebase_core }}" + firebase_analytics: "^{{ plugins.firebase_dynamic_links }}" +``` + + + + +```yaml {5} title="pubspec.yaml" +dependencies: + flutter: + sdk: flutter + firebase_core: "^{{ plugins.firebase_core }}" + firebase_dynamic_links: "^{{ plugins.firebase_dynamic_links_ns }}" +``` + + + + +### 2. Download dependency + +``` +$ flutter pub get +``` + +### 4. Rebuild your app + +Once complete, rebuild your Flutter application: + +```bash +$ flutter run +``` + +## Platform Integration + +Before using Dynamic Links, ensure you have configured your specific platform: + +- [Android](./android-integration.mdx) +- [Apple](./apple-integration.mdx) + +## Next steps + +Once your platforms have been configured, head over to the [Usage](./usage.mdx) documentation. diff --git a/docs/dynamic-links/usage.mdx b/docs/dynamic-links/usage.mdx index 269df4d4c1a6..6fb3a04940ab 100644 --- a/docs/dynamic-links/usage.mdx +++ b/docs/dynamic-links/usage.mdx @@ -3,4 +3,154 @@ title: Dynamic Links sidebar_label: Usage --- -Dynamic Links usage +To start using the Dynamic Links package within your project, import it at the top of your project files: + +```dart +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +``` + +Before using Dynamic Links, you must first have ensured you have [initialized FlutterFire](../overview.mdx#initializing-flutterfire). + +To create a new Dynamic Links instance, call the [`instance`](!firebase_dynamic_links.FirebaseDynamicLinks.instance) getter on [`FirebaseDynamicLinks`](!firebase_dynamic_links.FirebaseDynamicLinks): + +```dart +FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance; +``` + +By default, this allows you to interact with Dynamic Links using the default Firebase App used whilst installing FlutterFire on your +platform. + + +On `Android`, if you'd like to use Dynamic Links with a secondary Firebase App, use the [`instanceFor`](!firebase_dynamic_links.FirebaseDynamicLinks.instanceFor) method: + +```dart +// Android only +FirebaseApp secondaryApp = Firebase.app('SecondaryApp'); +FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instanceFor(app: secondaryApp); +``` + +## Setting up a prefix + +Before using Dynamic Links, ensure you have first created a new link prefix on the +[Firebase Console](https://console.firebase.google.com/project/_/durablelinks). + +For example, the `my-awesome-app.page.link` has been added on this project: + +Setting up a prefix + +## Create a Dynamic Link + +A dynamic link can be created directly from the Firebase Console, or programmatically via the `firebase_dynamic_links` plugin. Once a link has been created, +you can use send them to users (via emails, push notifications, in-app content etc.). Upon opening, your application can handle the link however you like, for +example opening a specific screen. + +### Build Dynamic Link + +To build a Dynamic Link, use the [`FirebaseDynamicLinks.buildLink`](!firebase_dynamic_links.buildLink) API in your application code like so: + +```dart +final DynamicLinkParameters parameters = DynamicLinkParameters( + // The Dynamic Link URI domain. You can view created URIs on your Firebase console + uriPrefix: 'https://my-awesome-app.page.link', + // The deep Link passed to your application which you can use to affect change + link: Uri.parse('https://www.example.com/view-to-open'), + // Android application details needed for opening correct app on device/Play Store + androidParameters: const AndroidParameters( + packageName: androidPackageName, + minimumVersion: 1, + ), + // iOS application details needed for opening correct app on device/App Store + iosParameters: const IOSParameters( + bundleId: iosBundleId, + minimumVersion: '2', + ), +); + +final Uri uri = await dynamicLinks.buildLink(parameters); +``` + +The method accepts a `DynamicLinkParameters` instance, which at a minimum requires a `uriPrefix` (defined in the Firebase Console), +along with a `link`, which is passed to your application when a user opens the app via a created link. + +### Build Short Dynamic Link + +You can also build a short Dynamic Link which simply makes the Dynamic Link URL shorter. This does entail an additional native SDK +request to the Firebase server whilst the above `buildLink()` does not. To build a short Dynamic Link, use the +[`FirebaseDynamicLinks.buildShortLink`](!firebase_dynamic_links.buildShortLink) API in your application code like so: + +```dart +final DynamicLinkParameters parameters = DynamicLinkParameters( + // The Dynamic Link URI domain. You can view created URIs on your Firebase console + uriPrefix: 'https://example.page.link', + // The deep Link passed to your application which you can use to affect change + link: Uri.parse('https://www.example.com/view-to-open'), + // Android application details needed for opening correct app on device/Play Store + androidParameters: const AndroidParameters( + packageName: androidPackageName, + minimumVersion: 1, + ), + // iOS application details needed for opening correct app on device/App Store + iosParameters: const IOSParameters( + bundleId: iosBundleId, + minimumVersion: '2', + ), +); + +final Uri uri = await FirebaseDynamicLinks.instance.buildShortLink(parameters); +``` + +## Handling Dynamic Links + +To handle a Dynamic Link in your application, two scenarios require implementing. + +### Background or Terminated + +If the application is in the background or terminated, the [`FirebaseDynamicLinks.getInitialLink`](!firebase_dynamic_links.getInitialLink) +method allows you to retrieve the Dynamic Link that opened the application or brought it to the foreground. + +This is an asynchronous request, so it makes sense to handle a link before rendering application logic, such as +a navigator. For example, you could handle this in the `main` function: + +```dart +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseConfig.platformOptions); + + // Get any initial links + final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getInitialLink(); + + runApp(MyApp(initialLink)); +} +``` + +Within your application logic, you can then check whether a link was handled and perform an action, for example: + +```dart +if (initialLink != null) { + final Uri deepLink = initialLink.link; + // Example of using the dynamic link to push the user to a different screen + Navigator.pushNamed(context, deepLink.path); +} +``` + +Alternatively, if you wish to identify if an exact Dynamic Link was used to open the application, pass it to +the `getDynamicLink` method instead: + +```dart +String link = 'https://dynamic-link-domain/ke2Qa'; + +final PendingDynamicLinkData? initialLink = await FirebaseDynamicLinks.instance.getDynamicLink(Uri.parse(link)); +``` + +## Listen for incoming links + +Whilst the application is open, you may listen to Dynamic Links using a stream handler. The [`FirebaseDynamicLinks.onLink`](!firebase_dynamic_links.onLink) +getter returns a `Stream` containing a `PendingDynamicLinkData`: + +```dart +FirebaseDynamicLinks.instance.onLink.listen((dynamicLinkData) { + Navigator.pushNamed(context, dynamicLinkData.link.path); +}).onError((error) { + // Handle errors +}); +``` diff --git a/docs/sidebars.js b/docs/sidebars.js index 1baafaf79b1e..9e299d505e28 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -95,6 +95,14 @@ module.exports = { toReferenceAPI("firebase_crashlytics"), toGithubExample("firebase_crashlytics"), ], + 'Dynamic Links': [ + "dynamic-links/overview", + "dynamic-links/android-integration", + "dynamic-links/apple-integration", + "dynamic-links/usage", + toReferenceAPI("firebase_dynamic_links"), + toGithubExample("firebase_dynamic_links"), + ], "Realtime Database": [ "database/overview", toReferenceAPI("firebase_database"), diff --git a/packages/firebase_dynamic_links/README.md b/packages/firebase_dynamic_links/README.md deleted file mode 100644 index e320725e17bf..000000000000 --- a/packages/firebase_dynamic_links/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# Google Dynamic Links for Firebase - -[![pub package](https://img.shields.io/pub/v/firebase_dynamic_links.svg)](https://pub.dev/packages/firebase_dynamic_links) - -A Flutter plugin to use the [Google Dynamic Links for Firebase API](https://firebase.google.com/docs/dynamic-links/). - -With Dynamic Links, your users get the best available experience for the platform they open your link on. If a user opens a Dynamic Link on iOS or Android, they can be taken directly to the linked content in your native app. If a user opens the same Dynamic Link in a desktop browser, they can be taken to the equivalent content on your website. - -In addition, Dynamic Links work across app installs: if a user opens a Dynamic Link on iOS or Android and doesn't have your app installed, the user can be prompted to install it; then, after installation, your app starts and can access the link. - -For Flutter plugins for other Firebase products, see [README.md](https://github.com/FirebaseExtended/flutterfire/blob/master/README.md). - -## Usage - -To use this plugin, add `firebase_dynamic_links` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). You must also configure firebase dynamic links for each platform project: Android and iOS (see the example folder for details). - -## Create Dynamic Links - -You create a Dynamic Link either by using the Firebase console, using a REST API, iOS or Android Builder API, Flutter API, or by forming a URL by adding Dynamic Link parameters to a URI prefix specific to your app. These parameters specify the links you want to open, depending on the user's platform and whether your app is installed. - -Below are instructions to create Dynamic Links using Flutter with the Firebase Dynamic Links API. This API accepts either a long Dynamic Link or an object containing Dynamic Link parameters, and returns a URL like the following example: - -``` -https://example.page.link/WXYZ -``` - -You can create a Dynamic Link programmatically by setting the following parameters and using the `DynamicLinkParameters.buildUrl()` method. - -```dart -final DynamicLinkParameters parameters = DynamicLinkParameters( - uriPrefix: 'https://abc123.app.goo.gl', - link: Uri.parse('https://example.com/'), - androidParameters: AndroidParameters( - packageName: 'com.example.android', - minimumVersion: 125, - ), - iosParameters: IosParameters( - bundleId: 'com.example.ios', - minimumVersion: '1.0.1', - appStoreId: '123456789', - ), - googleAnalyticsParameters: GoogleAnalyticsParameters( - campaign: 'example-promo', - medium: 'social', - source: 'orkut', - ), - itunesConnectAnalyticsParameters: ItunesConnectAnalyticsParameters( - providerToken: '123456', - campaignToken: 'example-promo', - ), - socialMetaTagParameters: SocialMetaTagParameters( - title: 'Example of a Dynamic Link', - description: 'This link works whether app is installed or not!', - ), -); - -final Uri dynamicUrl = await parameters.buildUrl(); -``` - -To create a short Dynamic Link, build `DynamicLinkParameters` the same way, but use the `DynamicLinkParameters.buildShortLink()` method. - -```dart -final ShortDynamicLink shortDynamicLink = await parameters.buildShortLink(); -final Uri shortUrl = shortDynamicLink.shortUrl; -``` - -To shorten a long Dynamic Link, use the DynamicLinkParameters.shortenUrl method. - -```dart -final ShortDynamicLink shortenedLink = await DynamicLinkParameters.shortenUrl( - Uri.parse('https://example.page.link/?link=https://example.com/&apn=com.example.android&ibi=com.example.ios'), - DynamicLinkParametersOptions(ShortDynamicLinkPathLength.unguessable), -); - -final Uri shortUrl = shortenedLink.shortUrl; -``` - -## Handle Received Dynamic Links - -You can receive a Dynamic Link containing a deep link that takes the user to specific content within your app: - -1. In the [Firebase Console](https://console.firebase.google.com), open the Dynamic Links section. - - Accept the terms of service if you are prompted to do so. - - Take note of your project's Dynamic Links URL prefix, which is displayed at the top of the Dynamic Links page. You need your project's Dynamic Links URL prefix to programmatically create Dynamic Links. Unless you are using a custom domain, a Dynamic Links URL prefix looks like `https://YOUR_SUBDOMAIN.page.link`. - -Receiving dynamic links on *iOS* requires a couple more steps than *Android*. If you only want to receive dynamic links on *Android*, skip to step 5. You can also follow a video on the next two steps [here.](https://youtu.be/KLBjAg6HvG0?t=60) - -2. In the **Info** tab of your *iOS* app's Xcode project: - - Create a new **URL Type** to be used for Dynamic Links. - - Set the **Identifier** field to a unique value and the **URL Schemes** field to be your bundle identifier, which is the default URL scheme used by Dynamic Links. - -3. In the **Capabilities** tab of your app's Xcode project, enable **Associated Domains** and add the following to the **Associated Domains** list: - -``` -applinks:YOUR_URL_PREFIX -``` - -Remember not to include `https://` or any slashes or paths in your prefix - -4. **If you are using a custom domain** create an key in your `Info.plist` file called `FirebaseDynamicLinksCustomDomains` and set it to your app's Dynamic Link URL prefixes. For example: - -```xml -FirebaseDynamicLinksCustomDomains - - https://example.com/promos - https://example.com/links/share - -``` -5. If you want to open android app directly without redirecting to Chrome or Any other Browser you can put an `intent-filter` in your `AndroidManifest.xml` file. It will open the system dialogue to open link with your app or other browsers. Users can then directly choose to open the link in your app. - -Note: This step is optional and in case we do not implement this then link will open in chrome at first and then will eventually open your application. -```xml - - - - - - - -``` -6. To receive a dynamic link, call the `getInitialLink()` method from `FirebaseDynamicLinks` which gets the link that opened the app (or null if it was not opened via a dynamic link) -and configure listeners for link callbacks when the application is active or in background calling `onLink`. - -```dart -void main() { - runApp(MaterialApp( - title: 'Dynamic Links Example', - routes: { - '/': (BuildContext context) => MyHomeWidget(), // Default home route - '/helloworld': (BuildContext context) => MyHelloWorldWidget(), - }, - )); -} - -class MyHomeWidgetState extends State { - . - . - . - @override - void initState() { - super.initState(); - this.initDynamicLinks(); - } - - void initDynamicLinks() async { - FirebaseDynamicLinks.instance.onLink( - onSuccess: (PendingDynamicLinkData? dynamicLink) async { - final Uri? deepLink = dynamicLink?.link; - - if (deepLink != null) { - Navigator.pushNamed(context, deepLink.path); - } - }, - onError: (OnLinkErrorException e) async { - print('onLinkError'); - print(e.message); - } - ); - - final PendingDynamicLinkData? data = await FirebaseDynamicLinks.instance.getInitialLink(); - final Uri? deepLink = data?.link; - - if (deepLink != null) { - Navigator.pushNamed(context, deepLink.path); - } - } - . - . - . -} -``` - -If your app did not open from a dynamic link, `getInitialLink()` will return `null`. - -## Getting Started - -See the `example` directory for a complete sample app using Google Dynamic Links for Firebase. - -## Issues and feedback - -Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/FirebaseExtended/flutterfire/issues/new). - -Plugin issues that are not specific to Flutterfire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). - -To contribute a change to this plugin, -please review our [contribution guide](https://github.com/FirebaseExtended/flutterfire/blob/master/CONTRIBUTING.md) -and open a [pull request](https://github.com/FirebaseExtended/flutterfire/pulls). diff --git a/packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FirebaseDynamicLinksPlugin.java b/packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FirebaseDynamicLinksPlugin.java deleted file mode 100644 index 7ec424a846fc..000000000000 --- a/packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FirebaseDynamicLinksPlugin.java +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.firebasedynamiclinks; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.OnFailureListener; -import com.google.android.gms.tasks.OnSuccessListener; -import com.google.android.gms.tasks.Task; -import com.google.firebase.dynamiclinks.DynamicLink; -import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; -import com.google.firebase.dynamiclinks.PendingDynamicLinkData; -import com.google.firebase.dynamiclinks.ShortDynamicLink; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.NewIntentListener; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Flutter plugin accessing for Firebase Dynamic Links API. - * - *

Instantiate this in an add to app scenario to gracefully handle activity and context changes. - */ -public class FirebaseDynamicLinksPlugin - implements FlutterPlugin, ActivityAware, MethodCallHandler, NewIntentListener { - private Activity activity; - private MethodChannel channel; - - private FirebaseDynamicLinksPlugin(Activity activity, MethodChannel channel) { - this.activity = activity; - this.channel = channel; - } - - /** - * Default Constructor. - * - *

Use this when adding the plugin to your FlutterEngine - */ - public FirebaseDynamicLinksPlugin() {} - - private static MethodChannel createChannel(final BinaryMessenger messenger) { - return new MethodChannel(messenger, "plugins.flutter.io/firebase_dynamic_links"); - } - - @Override - public boolean onNewIntent(Intent intent) { - FirebaseDynamicLinks.getInstance() - .getDynamicLink(intent) - .addOnSuccessListener( - new OnSuccessListener() { - @Override - public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { - if (pendingDynamicLinkData != null) { - Map dynamicLink = - getMapFromPendingDynamicLinkData(pendingDynamicLinkData); - channel.invokeMethod("onLinkSuccess", dynamicLink); - } - } - }) - .addOnFailureListener( - new OnFailureListener() { - @Override - public void onFailure(Exception e) { - Map exception = new HashMap<>(); - exception.put("code", e.getClass().getSimpleName()); - exception.put("message", e.getMessage()); - exception.put("details", null); - channel.invokeMethod("onLinkError", exception); - } - }); - - return false; - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - channel = createChannel(binding.getBinaryMessenger()); - channel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - activity = binding.getActivity(); - binding.addOnNewIntentListener(this); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - activity = null; - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - activity = binding.getActivity(); - binding.addOnNewIntentListener(this); - } - - @Override - public void onDetachedFromActivity() { - activity = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - switch (call.method) { - case "DynamicLinkParameters#buildUrl": - DynamicLink.Builder urlBuilder = setupParameters(call); - result.success(urlBuilder.buildDynamicLink().getUri().toString()); - break; - case "DynamicLinkParameters#buildShortLink": - DynamicLink.Builder shortLinkBuilder = setupParameters(call); - buildShortDynamicLink(shortLinkBuilder, call, createShortLinkListener(result)); - break; - case "DynamicLinkParameters#shortenUrl": - DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance().createDynamicLink(); - - Uri url = Uri.parse((String) call.argument("url")); - builder.setLongLink(url); - buildShortDynamicLink(builder, call, createShortLinkListener(result)); - break; - case "FirebaseDynamicLinks#getDynamicLink": - handleGetDynamicLink(result, Uri.parse((String) call.argument("url"))); - break; - case "FirebaseDynamicLinks#getInitialLink": - handleGetInitialDynamicLink(result); - break; - default: - result.notImplemented(); - break; - } - } - - private Map getMapFromPendingDynamicLinkData( - PendingDynamicLinkData pendingDynamicLinkData) { - Map dynamicLink = new HashMap<>(); - Uri link = pendingDynamicLinkData.getLink(); - dynamicLink.put("link", link != null ? link.toString() : null); - - Map androidData = new HashMap<>(); - androidData.put("clickTimestamp", pendingDynamicLinkData.getClickTimestamp()); - androidData.put("minimumVersion", pendingDynamicLinkData.getMinimumAppVersion()); - - dynamicLink.put("android", androidData); - return dynamicLink; - } - - private void addDynamicLinkListener(Task task, final Result result) { - task.addOnSuccessListener( - new OnSuccessListener() { - @Override - public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { - if (pendingDynamicLinkData != null) { - Map dynamicLink = - getMapFromPendingDynamicLinkData(pendingDynamicLinkData); - result.success(dynamicLink); - return; - } - result.success(null); - } - }) - .addOnFailureListener( - new OnFailureListener() { - @Override - public void onFailure(Exception e) { - result.error(e.getClass().getSimpleName(), e.getMessage(), null); - } - }); - } - - private void handleGetDynamicLink(final Result result, Uri uri) { - addDynamicLinkListener(FirebaseDynamicLinks.getInstance().getDynamicLink(uri), result); - } - - private void handleGetInitialDynamicLink(final Result result) { - // If there's no activity or initial Intent, then there's no initial dynamic link. - if (activity == null || activity.getIntent() == null) { - result.success(null); - return; - } - - addDynamicLinkListener( - FirebaseDynamicLinks.getInstance().getDynamicLink(activity.getIntent()), result); - } - - private OnCompleteListener createShortLinkListener(final Result result) { - return new OnCompleteListener() { - @Override - public void onComplete(Task task) { - if (task.isSuccessful()) { - Map url = new HashMap<>(); - url.put("url", task.getResult().getShortLink().toString()); - - List warnings = new ArrayList<>(); - if (task.getResult().getWarnings() != null) { - for (ShortDynamicLink.Warning warning : task.getResult().getWarnings()) { - warnings.add(warning.getMessage()); - } - } - - url.put("warnings", warnings); - - result.success(url); - } else { - Exception exception = task.getException(); - String errMsg = "Unable to create short link"; - if (exception != null && exception.getLocalizedMessage() != null) { - errMsg = exception.getLocalizedMessage(); - } - result.error("short_link_error", errMsg, null); - } - } - }; - } - - private void buildShortDynamicLink( - DynamicLink.Builder builder, MethodCall call, OnCompleteListener listener) { - Integer suffix = null; - - Map dynamicLinkParametersOptions = - call.argument("dynamicLinkParametersOptions"); - if (dynamicLinkParametersOptions != null) { - Integer shortDynamicLinkPathLength = - (Integer) dynamicLinkParametersOptions.get("shortDynamicLinkPathLength"); - if (shortDynamicLinkPathLength != null) { - switch (shortDynamicLinkPathLength) { - case 0: - suffix = ShortDynamicLink.Suffix.UNGUESSABLE; - break; - case 1: - suffix = ShortDynamicLink.Suffix.SHORT; - break; - default: - break; - } - } - } - - if (suffix != null) { - builder.buildShortDynamicLink(suffix).addOnCompleteListener(listener); - } else { - builder.buildShortDynamicLink().addOnCompleteListener(listener); - } - } - - private DynamicLink.Builder setupParameters(MethodCall call) { - DynamicLink.Builder dynamicLinkBuilder = FirebaseDynamicLinks.getInstance().createDynamicLink(); - - String uriPrefix = call.argument("uriPrefix"); - String link = call.argument("link"); - - dynamicLinkBuilder.setDomainUriPrefix(uriPrefix); - dynamicLinkBuilder.setLink(Uri.parse(link)); - - Map androidParameters = call.argument("androidParameters"); - if (androidParameters != null) { - String packageName = valueFor("packageName", androidParameters); - String fallbackUrl = valueFor("fallbackUrl", androidParameters); - Integer minimumVersion = valueFor("minimumVersion", androidParameters); - - DynamicLink.AndroidParameters.Builder builder = - new DynamicLink.AndroidParameters.Builder(packageName); - - if (fallbackUrl != null) builder.setFallbackUrl(Uri.parse(fallbackUrl)); - if (minimumVersion != null) builder.setMinimumVersion(minimumVersion); - - dynamicLinkBuilder.setAndroidParameters(builder.build()); - } - - Map googleAnalyticsParameters = call.argument("googleAnalyticsParameters"); - if (googleAnalyticsParameters != null) { - String campaign = valueFor("campaign", googleAnalyticsParameters); - String content = valueFor("content", googleAnalyticsParameters); - String medium = valueFor("medium", googleAnalyticsParameters); - String source = valueFor("source", googleAnalyticsParameters); - String term = valueFor("term", googleAnalyticsParameters); - - DynamicLink.GoogleAnalyticsParameters.Builder builder = - new DynamicLink.GoogleAnalyticsParameters.Builder(); - - if (campaign != null) builder.setCampaign(campaign); - if (content != null) builder.setContent(content); - if (medium != null) builder.setMedium(medium); - if (source != null) builder.setSource(source); - if (term != null) builder.setTerm(term); - - dynamicLinkBuilder.setGoogleAnalyticsParameters(builder.build()); - } - - Map iosParameters = call.argument("iosParameters"); - if (iosParameters != null) { - String bundleId = valueFor("bundleId", iosParameters); - String appStoreId = valueFor("appStoreId", iosParameters); - String customScheme = valueFor("customScheme", iosParameters); - String fallbackUrl = valueFor("fallbackUrl", iosParameters); - String ipadBundleId = valueFor("ipadBundleId", iosParameters); - String ipadFallbackUrl = valueFor("ipadFallbackUrl", iosParameters); - String minimumVersion = valueFor("minimumVersion", iosParameters); - - DynamicLink.IosParameters.Builder builder = new DynamicLink.IosParameters.Builder(bundleId); - - if (appStoreId != null) builder.setAppStoreId(appStoreId); - if (customScheme != null) builder.setCustomScheme(customScheme); - if (fallbackUrl != null) builder.setFallbackUrl(Uri.parse(fallbackUrl)); - if (ipadBundleId != null) builder.setIpadBundleId(ipadBundleId); - if (ipadFallbackUrl != null) builder.setIpadFallbackUrl(Uri.parse(ipadFallbackUrl)); - if (minimumVersion != null) builder.setMinimumVersion(minimumVersion); - - dynamicLinkBuilder.setIosParameters(builder.build()); - } - - Map itunesConnectAnalyticsParameters = - call.argument("itunesConnectAnalyticsParameters"); - if (itunesConnectAnalyticsParameters != null) { - String affiliateToken = valueFor("affiliateToken", itunesConnectAnalyticsParameters); - String campaignToken = valueFor("campaignToken", itunesConnectAnalyticsParameters); - String providerToken = valueFor("providerToken", itunesConnectAnalyticsParameters); - - DynamicLink.ItunesConnectAnalyticsParameters.Builder builder = - new DynamicLink.ItunesConnectAnalyticsParameters.Builder(); - - if (affiliateToken != null) builder.setAffiliateToken(affiliateToken); - if (campaignToken != null) builder.setCampaignToken(campaignToken); - if (providerToken != null) builder.setProviderToken(providerToken); - - dynamicLinkBuilder.setItunesConnectAnalyticsParameters(builder.build()); - } - - Map navigationInfoParameters = call.argument("navigationInfoParameters"); - if (navigationInfoParameters != null) { - Boolean forcedRedirectEnabled = valueFor("forcedRedirectEnabled", navigationInfoParameters); - - DynamicLink.NavigationInfoParameters.Builder builder = - new DynamicLink.NavigationInfoParameters.Builder(); - - if (forcedRedirectEnabled != null) builder.setForcedRedirectEnabled(forcedRedirectEnabled); - - dynamicLinkBuilder.setNavigationInfoParameters(builder.build()); - } - - Map socialMetaTagParameters = call.argument("socialMetaTagParameters"); - if (socialMetaTagParameters != null) { - String description = valueFor("description", socialMetaTagParameters); - String imageUrl = valueFor("imageUrl", socialMetaTagParameters); - String title = valueFor("title", socialMetaTagParameters); - - DynamicLink.SocialMetaTagParameters.Builder builder = - new DynamicLink.SocialMetaTagParameters.Builder(); - - if (description != null) builder.setDescription(description); - if (imageUrl != null) builder.setImageUrl(Uri.parse(imageUrl)); - if (title != null) builder.setTitle(title); - - dynamicLinkBuilder.setSocialMetaTagParameters(builder.build()); - } - - return dynamicLinkBuilder; - } - - private static T valueFor(String key, Map map) { - @SuppressWarnings("unchecked") - T result = (T) map.get(key); - return result; - } -} diff --git a/packages/firebase_dynamic_links/example/android/app/google-services.json b/packages/firebase_dynamic_links/example/android/app/google-services.json deleted file mode 100644 index 7123ea9b2ebd..000000000000 --- a/packages/firebase_dynamic_links/example/android/app/google-services.json +++ /dev/null @@ -1,113 +0,0 @@ -{ - "project_info": { - "project_number": "479882132969", - "firebase_url": "https://my-flutter-proj.firebaseio.com", - "project_id": "my-flutter-proj", - "storage_bucket": "my-flutter-proj.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139", - "android_client_info": { - "package_name": "io.flutter.plugins.firebasedynamiclinksexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.firebasedynamiclinksexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-pkn7lcq09ln9vfk4k52r634vh805dk3g.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "cvbxvzv" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ] - }, - "ads_service": { - "status": 2 - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b", - "android_client_info": { - "package_name": "io.flutter.plugins.firebaseperformanceexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.firebaseperformanceexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-pkn7lcq09ln9vfk4k52r634vh805dk3g.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "cvbxvzv" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/packages/firebase_dynamic_links/example/ios/Runner/GoogleService-Info.plist b/packages/firebase_dynamic_links/example/ios/Runner/GoogleService-Info.plist deleted file mode 100644 index ac0d4c98b0d0..000000000000 --- a/packages/firebase_dynamic_links/example/ios/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,42 +0,0 @@ - - - - - AD_UNIT_ID_FOR_BANNER_TEST - ca-app-pub-3940256099942544/2934735716 - AD_UNIT_ID_FOR_INTERSTITIAL_TEST - ca-app-pub-3940256099942544/4411468910 - CLIENT_ID - 479882132969-pn2ancg65o0e7r5ikte1qiciuvdghqf9.apps.googleusercontent.com - REVERSED_CLIENT_ID - com.googleusercontent.apps.479882132969-pn2ancg65o0e7r5ikte1qiciuvdghqf9 - ANDROID_CLIENT_ID - 479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com - API_KEY - AIzaSyBECOwLTAN6PU4Aet1b2QLGIb3kRK8Xjew - GCM_SENDER_ID - 479882132969 - PLIST_VERSION - 1 - BUNDLE_ID - com.google.FirebaseCppDynamicLinksTestApp.dev - PROJECT_ID - my-flutter-proj - STORAGE_BUCKET - my-flutter-proj.appspot.com - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:479882132969:ios:36e157824ba4dd3d - DATABASE_URL - https://my-flutter-proj.firebaseio.com - - \ No newline at end of file diff --git a/packages/firebase_dynamic_links/example/lib/main.dart b/packages/firebase_dynamic_links/example/lib/main.dart deleted file mode 100644 index 1426406be6ee..000000000000 --- a/packages/firebase_dynamic_links/example/lib/main.dart +++ /dev/null @@ -1,172 +0,0 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; - -void main() { - runApp(MaterialApp( - title: 'Dynamic Links Example', - routes: { - '/': (BuildContext context) => _MainScreen(), - '/helloworld': (BuildContext context) => _DynamicLinkScreen(), - }, - )); -} - -class _MainScreen extends StatefulWidget { - @override - State createState() => _MainScreenState(); -} - -class _MainScreenState extends State<_MainScreen> { - String? _linkMessage; - bool _isCreatingLink = false; - String _testString = - 'To test: long press link and then copy and click from a non-browser ' - "app. Make sure this isn't being tested on iOS simulator and iOS xcode " - 'is properly setup. Look at firebase_dynamic_links/README.md for more ' - 'details.'; - - @override - void initState() { - super.initState(); - initDynamicLinks(); - } - - Future initDynamicLinks() async { - FirebaseDynamicLinks.instance.onLink( - onSuccess: (PendingDynamicLinkData? dynamicLink) async { - final Uri? deepLink = dynamicLink?.link; - - if (deepLink != null) { - // ignore: unawaited_futures - Navigator.pushNamed(context, deepLink.path); - } - }, onError: (OnLinkErrorException e) async { - print('onLinkError'); - print(e.message); - }); - - final PendingDynamicLinkData? data = - await FirebaseDynamicLinks.instance.getInitialLink(); - final Uri? deepLink = data?.link; - - if (deepLink != null) { - // ignore: unawaited_futures - Navigator.pushNamed(context, deepLink.path); - } - } - - Future _createDynamicLink(bool short) async { - setState(() { - _isCreatingLink = true; - }); - - final DynamicLinkParameters parameters = DynamicLinkParameters( - uriPrefix: 'https://cx4k7.app.goo.gl', - link: Uri.parse('https://dynamic.link.example/helloworld'), - androidParameters: AndroidParameters( - packageName: 'io.flutter.plugins.firebasedynamiclinksexample', - minimumVersion: 0, - ), - dynamicLinkParametersOptions: DynamicLinkParametersOptions( - shortDynamicLinkPathLength: ShortDynamicLinkPathLength.short, - ), - iosParameters: IosParameters( - bundleId: 'com.google.FirebaseCppDynamicLinksTestApp.dev', - minimumVersion: '0', - ), - ); - - Uri url; - if (short) { - final ShortDynamicLink shortLink = await parameters.buildShortLink(); - url = shortLink.shortUrl; - } else { - url = await parameters.buildUrl(); - } - - setState(() { - _linkMessage = url.toString(); - _isCreatingLink = false; - }); - } - - @override - Widget build(BuildContext context) { - return Material( - child: Scaffold( - appBar: AppBar( - title: const Text('Dynamic Links Example'), - ), - body: Builder(builder: (BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ButtonBar( - alignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: !_isCreatingLink - ? () => _createDynamicLink(false) - : null, - child: const Text('Get Long Link'), - ), - ElevatedButton( - onPressed: !_isCreatingLink - ? () => _createDynamicLink(true) - : null, - child: const Text('Get Short Link'), - ), - ], - ), - InkWell( - onTap: () async { - if (_linkMessage != null) { - await launch(_linkMessage!); - } - }, - onLongPress: () { - Clipboard.setData(ClipboardData(text: _linkMessage)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Copied Link!')), - ); - }, - child: Text( - _linkMessage ?? '', - style: const TextStyle(color: Colors.blue), - ), - ), - Text(_linkMessage == null ? '' : _testString) - ], - ), - ); - }), - ), - ); - } -} - -class _DynamicLinkScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Material( - child: Scaffold( - appBar: AppBar( - title: const Text('Hello World DeepLink'), - ), - body: const Center( - child: Text('Hello, World!'), - ), - ), - ); - } -} diff --git a/packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart b/packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart deleted file mode 100644 index 91d298d479d0..000000000000 --- a/packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart +++ /dev/null @@ -1,73 +0,0 @@ -// ignore_for_file: require_trailing_commas -// @dart = 2.9 -// Copyright 2020, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:drive/drive.dart' as drive; -import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void testsMain() { - group('DynamicLinks', () { - test('buildUrl', () async { - const String androidPackageName = - 'io.flutter.plugins.firebasedynamiclinksexample'; - const String iosBundleId = - 'com.google.FirebaseCppDynamicLinksTestApp.dev'; - const String urlHost = 'cx4k7.app.goo.gl'; - const String link = 'https://dynamic.link.example/helloworld'; - - final DynamicLinkParameters parameters = DynamicLinkParameters( - uriPrefix: 'https://$urlHost', - link: Uri.parse(link), - androidParameters: AndroidParameters( - packageName: androidPackageName, - minimumVersion: 1, - ), - dynamicLinkParametersOptions: DynamicLinkParametersOptions( - shortDynamicLinkPathLength: ShortDynamicLinkPathLength.short, - ), - iosParameters: IosParameters( - bundleId: iosBundleId, - minimumVersion: '2', - ), - ); - - final Uri uri = await parameters.buildUrl(); - - // androidParameters.minimumVersion - expect( - uri.queryParameters['amv'], - '1', - ); - // iosParameters.minimumVersion - expect( - uri.queryParameters['imv'], - '2', - ); - // androidParameters.packageName - expect( - uri.queryParameters['apn'], - androidPackageName, - ); - // iosParameters.bundleId - expect( - uri.queryParameters['ibi'], - iosBundleId, - ); - // link - expect( - uri.queryParameters['link'], - Uri.encodeFull(link), - ); - // uriPrefix - expect( - uri.host, - urlHost, - ); - }); - }); -} - -void main() => drive.main(testsMain); diff --git a/packages/firebase_dynamic_links/CHANGELOG.md b/packages/firebase_dynamic_links/firebase_dynamic_links/CHANGELOG.md similarity index 88% rename from packages/firebase_dynamic_links/CHANGELOG.md rename to packages/firebase_dynamic_links/firebase_dynamic_links/CHANGELOG.md index 586965aaf723..5b5b40c9e292 100644 --- a/packages/firebase_dynamic_links/CHANGELOG.md +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/CHANGELOG.md @@ -1,3 +1,17 @@ +## UNRELEASED + +Overall, Firebase Dynamic Links has been heavily reworked to bring it inline with the federated plugin setup along with adding new features, +documentation and updating unit and end-to-end tests. + +- **`FirebaseDynamicLinks`** + - **BREAKING**: `onLink()` method has been removed. Instead, use `onLink` getter, it returns a `Stream`; events & errors are now streamed to the user. + - **BREAKING**: `DynamicLinkParameters` class has been removed. `buildLink()` (replaces `buildUrl()`) & `buildShortLink()` methods are now found on `FirebaseDynamicLinks.instance`. + - **BREAKING**: `DynamicLinkParameters.shortenUrl()` has been removed. + - **NEW**: `buildLink()` which replaces the previous `DynamicLinkParameters().buildUrl()`. + - **NEW**: `buildShortLink()` which replaces the previous `DynamicLinkParameters().buildShortLink()`. + - **NEW**: `DynamicLinkParameters` class is used to build parameters for `buildLink()` & `buildShortLink()`. + - **NEW**: Multi-app support now available for Android only using `FirebaseDynamicLinks.instanceFor()`. + ## 3.0.2 - Update a dependency to the latest release. diff --git a/packages/firebase_dynamic_links/LICENSE b/packages/firebase_dynamic_links/firebase_dynamic_links/LICENSE similarity index 100% rename from packages/firebase_dynamic_links/LICENSE rename to packages/firebase_dynamic_links/firebase_dynamic_links/LICENSE diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/README.md b/packages/firebase_dynamic_links/firebase_dynamic_links/README.md new file mode 100644 index 000000000000..87c26743522d --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/README.md @@ -0,0 +1,26 @@ + +# Firebase Dynamic Links for Flutter + +[![pub package](https://img.shields.io/pub/v/firebase_dynamic_links.svg)](https://pub.dev/packages/firebase_dynamic_links) + +A Flutter plugin to use the [Firebase Dynamic Links API](https://firebase.google.com/docs/dynamic-links/). + +To learn more about Dynamic Links, please visit the [Firebase website](https://firebase.google.com/products/dynamic-links) + +## Getting Started + +To get started with Dynamic Links for Flutter, please [see the documentation](https://firebase.flutter.dev/docs/dynamic-links/overview). + +## Usage + +To use this plugin, please visit the [Dynamic Links Usage documentation](https://firebase.flutter.dev/docs/dynamic-links/usage) + +## Issues and feedback + +Please file FlutterFire specific issues, bugs, or feature requests in our [issue tracker](https://github.com/FirebaseExtended/flutterfire/issues/new). + +Plugin issues that are not specific to FlutterFire can be filed in the [Flutter issue tracker](https://github.com/flutter/flutter/issues/new). + +To contribute a change to this plugin, +please review our [contribution guide](https://github.com/FirebaseExtended/flutterfire/blob/master/CONTRIBUTING.md) +and open a [pull request](https://github.com/FirebaseExtended/flutterfire/pulls). diff --git a/packages/firebase_dynamic_links/android/build.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/android/build.gradle similarity index 53% rename from packages/firebase_dynamic_links/android/build.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/android/build.gradle index fe5acb11f5b7..392261047a2e 100644 --- a/packages/firebase_dynamic_links/android/build.gradle +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/build.gradle @@ -1,22 +1,22 @@ -group 'io.flutter.plugins.firebasedynamiclinks' +group 'io.flutter.plugins.firebase.dynamiclinks' version '1.0-SNAPSHOT' buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.4' - } + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + } } rootProject.allprojects { - repositories { - google() - mavenCentral() - } + repositories { + google() + mavenCentral() + } } apply plugin: 'com.android.library' @@ -35,20 +35,23 @@ def getRootProjectExtOrCoreProperty(name, firebaseCoreProject) { } android { - compileSdkVersion 29 - - defaultConfig { - minSdkVersion 19 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - dependencies { - api firebaseCoreProject - implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") - implementation 'com.google.firebase:firebase-dynamic-links' - } + compileSdkVersion 31 + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } + dependencies { + api firebaseCoreProject + implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}") + implementation 'com.google.firebase:firebase-dynamic-links' + } } apply from: file("./user-agent.gradle") diff --git a/packages/firebase_dynamic_links/android/settings.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/android/settings.gradle similarity index 100% rename from packages/firebase_dynamic_links/android/settings.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/android/settings.gradle diff --git a/packages/firebase_dynamic_links/android/src/main/AndroidManifest.xml b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/AndroidManifest.xml similarity index 77% rename from packages/firebase_dynamic_links/android/src/main/AndroidManifest.xml rename to packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/AndroidManifest.xml index 663e4f0f3de9..376bea6cd392 100644 --- a/packages/firebase_dynamic_links/android/src/main/AndroidManifest.xml +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/AndroidManifest.xml @@ -1,10 +1,10 @@ + package="io.flutter.plugins.firebase.dynamiclinks"> - diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Constants.java b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Constants.java new file mode 100644 index 000000000000..ccbfc4efa5d4 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Constants.java @@ -0,0 +1,6 @@ +package io.flutter.plugins.firebase.dynamiclinks; + +public class Constants { + public static final String APP_NAME = "appName"; + public static final String DEFAULT_ERROR_CODE = "firebase_dynamic_links"; +} diff --git a/packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FlutterFirebaseAppRegistrar.java b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseAppRegistrar.java similarity index 93% rename from packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FlutterFirebaseAppRegistrar.java rename to packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseAppRegistrar.java index dbf3822c7a24..329ba3fc981f 100644 --- a/packages/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebasedynamiclinks/FlutterFirebaseAppRegistrar.java +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseAppRegistrar.java @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -package io.flutter.plugins.firebasedynamiclinks; +package io.flutter.plugins.firebase.dynamiclinks; import androidx.annotation.Keep; import com.google.firebase.components.Component; diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseDynamicLinksPlugin.java b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseDynamicLinksPlugin.java new file mode 100644 index 000000000000..ecd806601122 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/FlutterFirebaseDynamicLinksPlugin.java @@ -0,0 +1,369 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.firebase.dynamiclinks; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.dynamiclinks.DynamicLink; +import com.google.firebase.dynamiclinks.FirebaseDynamicLinks; +import com.google.firebase.dynamiclinks.PendingDynamicLinkData; +import com.google.firebase.dynamiclinks.ShortDynamicLink; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.PluginRegistry.NewIntentListener; +import io.flutter.plugins.firebase.core.FlutterFirebasePlugin; +import io.flutter.plugins.firebase.core.FlutterFirebasePluginRegistry; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +public class FlutterFirebaseDynamicLinksPlugin + implements FlutterFirebasePlugin, + FlutterPlugin, + ActivityAware, + MethodCallHandler, + NewIntentListener { + private final AtomicReference activity = new AtomicReference<>(null); + + private MethodChannel channel; + @Nullable private BinaryMessenger messenger; + + private static final String METHOD_CHANNEL_NAME = "plugins.flutter.io/firebase_dynamic_links"; + + private void initInstance(BinaryMessenger messenger) { + channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME); + channel.setMethodCallHandler(this); + FlutterFirebasePluginRegistry.registerPlugin(METHOD_CHANNEL_NAME, this); + + this.messenger = messenger; + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + initInstance(binding.getBinaryMessenger()); + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + channel = null; + messenger = null; + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + activity.set(binding.getActivity()); + binding.addOnNewIntentListener(this); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + detachToActivity(); + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + activity.set(binding.getActivity()); + binding.addOnNewIntentListener(this); + } + + private void detachToActivity() { + activity.set(null); + } + + @Override + public void onDetachedFromActivity() { + detachToActivity(); + } + + static FirebaseDynamicLinks getDynamicLinkInstance(@Nullable Map arguments) { + if (arguments != null) { + String appName = (String) arguments.get(Constants.APP_NAME); + if (appName != null) { + FirebaseApp app = FirebaseApp.getInstance(appName); + return FirebaseDynamicLinks.getInstance(app); + } + } + + return FirebaseDynamicLinks.getInstance(); + } + + @Override + public boolean onNewIntent(Intent intent) { + getDynamicLinkInstance(null) + .getDynamicLink(intent) + .addOnSuccessListener( + pendingDynamicLinkData -> { + Map dynamicLink = + Utils.getMapFromPendingDynamicLinkData(pendingDynamicLinkData); + if (dynamicLink != null) { + channel.invokeMethod("FirebaseDynamicLink#onLinkSuccess", dynamicLink); + } + }) + .addOnFailureListener( + exception -> + channel.invokeMethod( + "FirebaseDynamicLink#onLinkError", Utils.getExceptionDetails(exception))); + return false; + } + + @Override + public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result result) { + Task methodCallTask; + FirebaseDynamicLinks dynamicLinks = getDynamicLinkInstance(call.arguments()); + + switch (call.method) { + case "FirebaseDynamicLinks#buildLink": + String url = buildLink(call.arguments()); + result.success(url); + return; + case "FirebaseDynamicLinks#buildShortLink": + DynamicLink.Builder urlBuilder = setupParameters(call.arguments()); + methodCallTask = buildShortLink(urlBuilder, call.arguments()); + break; + case "FirebaseDynamicLinks#getDynamicLink": + case "FirebaseDynamicLinks#getInitialLink": + methodCallTask = getDynamicLink(dynamicLinks, call.argument("url")); + break; + default: + result.notImplemented(); + return; + } + + methodCallTask.addOnCompleteListener( + task -> { + if (task.isSuccessful()) { + result.success(task.getResult()); + } else { + Exception exception = task.getException(); + result.error( + Constants.DEFAULT_ERROR_CODE, + exception != null ? exception.getMessage() : null, + io.flutter.plugins.firebase.dynamiclinks.Utils.getExceptionDetails(exception)); + } + }); + } + + private String buildLink(Map arguments) { + DynamicLink.Builder urlBuilder = setupParameters(arguments); + + return urlBuilder.buildDynamicLink().getUri().toString(); + } + + private Task> buildShortLink( + DynamicLink.Builder urlBuilder, @Nullable Map arguments) { + return Tasks.call( + cachedThreadPool, + () -> { + Integer suffix = 1; + Integer shortDynamicLinkPathLength = (Integer) arguments.get("shortLinkType"); + if (shortDynamicLinkPathLength != null) { + switch (shortDynamicLinkPathLength) { + case 0: + suffix = ShortDynamicLink.Suffix.UNGUESSABLE; + break; + case 1: + suffix = ShortDynamicLink.Suffix.SHORT; + break; + default: + break; + } + } + + Map result = new HashMap<>(); + ShortDynamicLink shortLink; + if (suffix != null) { + shortLink = Tasks.await(urlBuilder.buildShortDynamicLink(suffix)); + } else { + shortLink = Tasks.await(urlBuilder.buildShortDynamicLink()); + } + + List warnings = new ArrayList<>(); + + for (ShortDynamicLink.Warning warning : shortLink.getWarnings()) { + warnings.add(warning.getMessage()); + } + + result.put("url", shortLink.getShortLink().toString()); + result.put("warnings", warnings); + result.put("previewLink", shortLink.getPreviewLink().toString()); + + return result; + }); + } + + private Task> getDynamicLink( + FirebaseDynamicLinks dynamicLinks, @Nullable String url) { + return Tasks.call( + cachedThreadPool, + () -> { + PendingDynamicLinkData pendingDynamicLink; + + if (url != null) { + pendingDynamicLink = Tasks.await(dynamicLinks.getDynamicLink(Uri.parse(url))); + } else { + // If there's no activity or initial Intent, then there's no initial dynamic link. + if (activity.get() == null || activity.get().getIntent() == null) { + return null; + } + pendingDynamicLink = + Tasks.await(dynamicLinks.getDynamicLink(activity.get().getIntent())); + } + + return Utils.getMapFromPendingDynamicLinkData(pendingDynamicLink); + }); + } + + private DynamicLink.Builder setupParameters(Map arguments) { + DynamicLink.Builder dynamicLinkBuilder = getDynamicLinkInstance(arguments).createDynamicLink(); + + String uriPrefix = (String) arguments.get("uriPrefix"); + String link = (String) arguments.get("link"); + + dynamicLinkBuilder.setDomainUriPrefix(uriPrefix); + dynamicLinkBuilder.setLink(Uri.parse(link)); + + Map androidParameters = + (Map) arguments.get("androidParameters"); + if (androidParameters != null) { + String packageName = valueFor("packageName", androidParameters); + String fallbackUrl = valueFor("fallbackUrl", androidParameters); + Integer minimumVersion = valueFor("minimumVersion", androidParameters); + + DynamicLink.AndroidParameters.Builder builder = + new DynamicLink.AndroidParameters.Builder(packageName); + + if (fallbackUrl != null) builder.setFallbackUrl(Uri.parse(fallbackUrl)); + if (minimumVersion != null) builder.setMinimumVersion(minimumVersion); + + dynamicLinkBuilder.setAndroidParameters(builder.build()); + } + + Map googleAnalyticsParameters = + (Map) arguments.get("googleAnalyticsParameters"); + if (googleAnalyticsParameters != null) { + String campaign = valueFor("campaign", googleAnalyticsParameters); + String content = valueFor("content", googleAnalyticsParameters); + String medium = valueFor("medium", googleAnalyticsParameters); + String source = valueFor("source", googleAnalyticsParameters); + String term = valueFor("term", googleAnalyticsParameters); + + DynamicLink.GoogleAnalyticsParameters.Builder builder = + new DynamicLink.GoogleAnalyticsParameters.Builder(); + + if (campaign != null) builder.setCampaign(campaign); + if (content != null) builder.setContent(content); + if (medium != null) builder.setMedium(medium); + if (source != null) builder.setSource(source); + if (term != null) builder.setTerm(term); + + dynamicLinkBuilder.setGoogleAnalyticsParameters(builder.build()); + } + + Map iosParameters = (Map) arguments.get("iosParameters"); + if (iosParameters != null) { + String bundleId = valueFor("bundleId", iosParameters); + String appStoreId = valueFor("appStoreId", iosParameters); + String customScheme = valueFor("customScheme", iosParameters); + String fallbackUrl = valueFor("fallbackUrl", iosParameters); + String ipadBundleId = valueFor("ipadBundleId", iosParameters); + String ipadFallbackUrl = valueFor("ipadFallbackUrl", iosParameters); + String minimumVersion = valueFor("minimumVersion", iosParameters); + + DynamicLink.IosParameters.Builder builder = new DynamicLink.IosParameters.Builder(bundleId); + + if (appStoreId != null) builder.setAppStoreId(appStoreId); + if (customScheme != null) builder.setCustomScheme(customScheme); + if (fallbackUrl != null) builder.setFallbackUrl(Uri.parse(fallbackUrl)); + if (ipadBundleId != null) builder.setIpadBundleId(ipadBundleId); + if (ipadFallbackUrl != null) builder.setIpadFallbackUrl(Uri.parse(ipadFallbackUrl)); + if (minimumVersion != null) builder.setMinimumVersion(minimumVersion); + + dynamicLinkBuilder.setIosParameters(builder.build()); + } + + Map itunesConnectAnalyticsParameters = + (Map) arguments.get("itunesConnectAnalyticsParameters"); + if (itunesConnectAnalyticsParameters != null) { + String affiliateToken = valueFor("affiliateToken", itunesConnectAnalyticsParameters); + String campaignToken = valueFor("campaignToken", itunesConnectAnalyticsParameters); + String providerToken = valueFor("providerToken", itunesConnectAnalyticsParameters); + + DynamicLink.ItunesConnectAnalyticsParameters.Builder builder = + new DynamicLink.ItunesConnectAnalyticsParameters.Builder(); + + if (affiliateToken != null) builder.setAffiliateToken(affiliateToken); + if (campaignToken != null) builder.setCampaignToken(campaignToken); + if (providerToken != null) builder.setProviderToken(providerToken); + + dynamicLinkBuilder.setItunesConnectAnalyticsParameters(builder.build()); + } + + Map navigationInfoParameters = + (Map) arguments.get("navigationInfoParameters"); + if (navigationInfoParameters != null) { + Boolean forcedRedirectEnabled = valueFor("forcedRedirectEnabled", navigationInfoParameters); + + DynamicLink.NavigationInfoParameters.Builder builder = + new DynamicLink.NavigationInfoParameters.Builder(); + + if (forcedRedirectEnabled != null) builder.setForcedRedirectEnabled(forcedRedirectEnabled); + + dynamicLinkBuilder.setNavigationInfoParameters(builder.build()); + } + + Map socialMetaTagParameters = + (Map) arguments.get("socialMetaTagParameters"); + if (socialMetaTagParameters != null) { + String description = valueFor("description", socialMetaTagParameters); + String imageUrl = valueFor("imageUrl", socialMetaTagParameters); + String title = valueFor("title", socialMetaTagParameters); + + DynamicLink.SocialMetaTagParameters.Builder builder = + new DynamicLink.SocialMetaTagParameters.Builder(); + + if (description != null) builder.setDescription(description); + if (imageUrl != null) builder.setImageUrl(Uri.parse(imageUrl)); + if (title != null) builder.setTitle(title); + + dynamicLinkBuilder.setSocialMetaTagParameters(builder.build()); + } + + return dynamicLinkBuilder; + } + + private static T valueFor(String key, Map map) { + @SuppressWarnings("unchecked") + T result = (T) map.get(key); + return result; + } + + @Override + public Task> getPluginConstantsForFirebaseApp(FirebaseApp firebaseApp) { + return Tasks.call(cachedThreadPool, () -> null); + } + + @Override + public Task didReinitializeFirebaseCore() { + return Tasks.call( + cachedThreadPool, + () -> { + return null; + }); + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Utils.java b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Utils.java new file mode 100644 index 000000000000..78ca8670c922 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/android/src/main/java/io/flutter/plugins/firebase/dynamiclinks/Utils.java @@ -0,0 +1,53 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.firebase.dynamiclinks; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.firebase.dynamiclinks.PendingDynamicLinkData; +import java.util.HashMap; +import java.util.Map; + +public class Utils { + static Map getExceptionDetails(@Nullable Exception exception) { + Map details = new HashMap<>(); + // There aren't any Dynamic Link Exceptions in the reference: + // https://firebase.google.com/docs/reference/android/com/google/firebase/dynamiclinks/package-summary + details.put("code", "unknown"); + if (exception != null) { + details.put("message", exception.getMessage()); + } else { + details.put("message", "An unknown error has occurred."); + } + return details; + } + + static Map getMapFromPendingDynamicLinkData( + PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData == null) { + return null; + } + + Map dynamicLink = new HashMap<>(); + + Uri link = pendingDynamicLinkData.getLink(); + dynamicLink.put("link", link != null ? link.toString() : null); + + Map utmParameters = new HashMap<>(); + + for (String key : pendingDynamicLinkData.getUtmParameters().keySet()) { + utmParameters.put(key, pendingDynamicLinkData.getUtmParameters().get(key).toString()); + } + + dynamicLink.put("utmParameters", utmParameters); + + Map androidData = new HashMap<>(); + androidData.put("clickTimestamp", pendingDynamicLinkData.getClickTimestamp()); + androidData.put("minimumVersion", pendingDynamicLinkData.getMinimumAppVersion()); + + dynamicLink.put("android", androidData); + return dynamicLink; + } +} diff --git a/packages/firebase_dynamic_links/android/user-agent.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/android/user-agent.gradle similarity index 100% rename from packages/firebase_dynamic_links/android/user-agent.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/android/user-agent.gradle diff --git a/packages/firebase_dynamic_links/example/.metadata b/packages/firebase_dynamic_links/firebase_dynamic_links/example/.metadata similarity index 100% rename from packages/firebase_dynamic_links/example/.metadata rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/.metadata diff --git a/packages/firebase_dynamic_links/example/README.md b/packages/firebase_dynamic_links/firebase_dynamic_links/example/README.md similarity index 100% rename from packages/firebase_dynamic_links/example/README.md rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/README.md diff --git a/packages/firebase_dynamic_links/example/analysis_options.yaml b/packages/firebase_dynamic_links/firebase_dynamic_links/example/analysis_options.yaml similarity index 75% rename from packages/firebase_dynamic_links/example/analysis_options.yaml rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/analysis_options.yaml index 98eee62ba0f0..9a1bfe522b63 100644 --- a/packages/firebase_dynamic_links/example/analysis_options.yaml +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/analysis_options.yaml @@ -1,8 +1,8 @@ # Copyright 2021 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # in the LICENSE file. -include: ../../../analysis_options.yaml +include: ../../../../analysis_options.yaml linter: rules: public_member_api_docs: false - avoid_print: false \ No newline at end of file + avoid_print: false diff --git a/packages/firebase_dynamic_links/example/android/app/build.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/build.gradle similarity index 87% rename from packages/firebase_dynamic_links/example/android/app/build.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/build.gradle index 9ed11651323b..d2dbe6457e6e 100644 --- a/packages/firebase_dynamic_links/example/android/app/build.gradle +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/build.gradle @@ -22,20 +22,19 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' -apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 31 lintOptions { disable 'InvalidPackage' } defaultConfig { - applicationId "io.flutter.plugins.firebasedynamiclinksexample" + applicationId "io.flutter.plugins.firebase.dynamiclinksexample" minSdkVersion 19 - targetSdkVersion 29 + targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/firebase_dynamic_links/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml similarity index 96% rename from packages/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml index 3906eecd9efa..3609fff1be49 100644 --- a/packages/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="io.flutter.plugins.firebase.dynamiclinksexample"> diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/drawable/launch_background.xml b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/firebase_dynamic_links/example/android/app/src/main/res/values/styles.xml b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/firebase_dynamic_links/example/android/app/src/main/res/values/styles.xml rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/app/src/main/res/values/styles.xml diff --git a/packages/firebase_dynamic_links/example/android/build.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/build.gradle similarity index 100% rename from packages/firebase_dynamic_links/example/android/build.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/build.gradle diff --git a/packages/firebase_dynamic_links/example/android/gradle.properties b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/gradle.properties similarity index 100% rename from packages/firebase_dynamic_links/example/android/gradle.properties rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/gradle.properties diff --git a/packages/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from packages/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties index 3c46198fce9e..297f2fec363f 100644 --- a/packages/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/packages/firebase_dynamic_links/example/android/settings.gradle b/packages/firebase_dynamic_links/firebase_dynamic_links/example/android/settings.gradle similarity index 100% rename from packages/firebase_dynamic_links/example/android/settings.gradle rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/android/settings.gradle diff --git a/packages/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist similarity index 97% rename from packages/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/firebase_dynamic_links/example/ios/Flutter/Debug.xcconfig b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Flutter/Debug.xcconfig rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/Debug.xcconfig diff --git a/packages/firebase_dynamic_links/example/ios/Flutter/Release.xcconfig b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Flutter/Release.xcconfig rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Flutter/Release.xcconfig diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/GoogleService-Info.plist b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/GoogleService-Info.plist new file mode 100644 index 000000000000..0c11fd920572 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/GoogleService-Info.plist @@ -0,0 +1,38 @@ + + + + + CLIENT_ID + 448618578101-4km55qmv55tguvnivgjdiegb3r0jquv5.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.448618578101-4km55qmv55tguvnivgjdiegb3r0jquv5 + ANDROID_CLIENT_ID + 448618578101-26jgjs0rtl4ts2i667vjb28kldvs2kp6.apps.googleusercontent.com + API_KEY + AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0 + GCM_SENDER_ID + 448618578101 + PLIST_VERSION + 1 + BUNDLE_ID + io.invertase.testing + PROJECT_ID + react-native-firebase-testing + STORAGE_BUCKET + react-native-firebase-testing.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:448618578101:ios:3e76955ab6d49ecaac3efc + DATABASE_URL + https://react-native-firebase-testing.firebaseio.com + + diff --git a/packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj similarity index 93% rename from packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj index 9a7df42960b3..97e00a42ac00 100644 --- a/packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 8FE60D1920C0959F00E3A541 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8FE60D1820C0959F00E3A541 /* GoogleService-Info.plist */; }; + 46EC2B98274648FE00B16F88 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 46EC2B97274648FE00B16F88 /* GoogleService-Info.plist */; }; 8FF283695FD42FAFAA6F2588 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BEA489D8A0A4C9E6F14F37D /* libPods-Runner.a */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; @@ -40,10 +40,10 @@ 23C87C2196BFAAA7E465A745 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3BEA489D8A0A4C9E6F14F37D /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 46EC2B97274648FE00B16F88 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8FE60D1820C0959F00E3A541 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 8FE60D1A20C0962300E3A541 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -81,6 +81,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 46EC2B97274648FE00B16F88 /* GoogleService-Info.plist */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, @@ -102,7 +103,6 @@ children = ( 8FE60D1A20C0962300E3A541 /* Runner.entitlements */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 8FE60D1820C0959F00E3A541 /* GoogleService-Info.plist */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -154,7 +154,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 852BEAA0EB8653502C27C0BD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -176,6 +175,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = YYX2P3XVJ7; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.SafariKeychain = { @@ -190,6 +190,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -209,12 +210,12 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 8FE60D1920C0959F00E3A541 /* GoogleService-Info.plist in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + 46EC2B98274648FE00B16F88 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -235,24 +236,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 852BEAA0EB8653502C27C0BD /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -322,7 +305,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -376,7 +358,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -427,10 +408,11 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -442,7 +424,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseCppDynamicLinksTestApp.dev; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -455,10 +437,11 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = YYX2P3XVJ7; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -470,7 +453,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseCppDynamicLinksTestApp.dev; + PRODUCT_BUNDLE_IDENTIFIER = io.invertase.testing; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 71% rename from packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/firebase_dynamic_links/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/firebase_dynamic_links/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/firebase_dynamic_links/example/ios/Runner/AppDelegate.h b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/AppDelegate.h rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/AppDelegate.h diff --git a/packages/firebase_dynamic_links/example/ios/Runner/AppDelegate.m b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/AppDelegate.m rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/AppDelegate.m diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Base.lproj/Main.storyboard b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Info.plist b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Info.plist similarity index 96% rename from packages/firebase_dynamic_links/example/ios/Runner/Info.plist rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Info.plist index f24bc9d4f80d..4b8da07a8393 100644 --- a/packages/firebase_dynamic_links/example/ios/Runner/Info.plist +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Info.plist @@ -27,7 +27,7 @@ Bundle ID CFBundleURLSchemes - com.google.FirebaseCppDynamicLinksTestApp.dev + io.invertase.testing diff --git a/packages/firebase_dynamic_links/example/ios/Runner/Runner.entitlements b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Runner.entitlements similarity index 54% rename from packages/firebase_dynamic_links/example/ios/Runner/Runner.entitlements rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Runner.entitlements index 0c67376ebacb..7c0178128b5f 100644 --- a/packages/firebase_dynamic_links/example/ios/Runner/Runner.entitlements +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/Runner.entitlements @@ -1,5 +1,10 @@ - + + com.apple.developer.associated-domains + + applinks:reactnativefirebase.page.link + + diff --git a/packages/firebase_dynamic_links/example/ios/Runner/main.m b/packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/main.m similarity index 100% rename from packages/firebase_dynamic_links/example/ios/Runner/main.m rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/ios/Runner/main.m diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/example/lib/main.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/example/lib/main.dart new file mode 100644 index 000000000000..259a6c9989dd --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/lib/main.dart @@ -0,0 +1,211 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' show Platform; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + if (Platform.isAndroid) { + // Android will work via Dart initialisation + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: 'AIzaSyAHAsf51D0A407EklG1bs-5wA7EbyfNFg0', + appId: '1:448618578101:ios:3e76955ab6d49ecaac3efc', + messagingSenderId: '448618578101', + projectId: 'react-native-firebase-testing', + authDomain: 'react-native-firebase-testing.firebaseapp.com', + iosClientId: + '448618578101-4km55qmv55tguvnivgjdiegb3r0jquv5.apps.googleusercontent.com', + ), + ); + } else { + // iOS requires that there is a GoogleService-Info.plist otherwise getInitialLink & getDynamicLink will not work correctly. + // iOS also requires you run in release mode to test dynamic links ("flutter run --release"). + await Firebase.initializeApp(); + } + runApp( + MaterialApp( + title: 'Dynamic Links Example', + routes: { + '/': (BuildContext context) => _MainScreen(), + '/helloworld': (BuildContext context) => _DynamicLinkScreen(), + }, + ), + ); +} + +class _MainScreen extends StatefulWidget { + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State<_MainScreen> { + String? _linkMessage; + bool _isCreatingLink = false; + + FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance; + final String _testString = + 'To test: long press link and then copy and click from a non-browser ' + "app. Make sure this isn't being tested on iOS simulator and iOS xcode " + 'is properly setup. Look at firebase_dynamic_links/README.md for more ' + 'details.'; + + final String DynamicLink = 'https://test-app/helloworld'; + final String Link = 'https://reactnativefirebase.page.link/bFkn'; + + @override + void initState() { + super.initState(); + initDynamicLinks(); + } + + Future initDynamicLinks() async { + dynamicLinks.onLink.listen((dynamicLinkData) { + Navigator.pushNamed(context, dynamicLinkData.link.path); + }).onError((error) { + print('onLink error'); + print(error.message); + }); + } + + Future _createDynamicLink(bool short) async { + setState(() { + _isCreatingLink = true; + }); + + final DynamicLinkParameters parameters = DynamicLinkParameters( + uriPrefix: 'https://reactnativefirebase.page.link', + link: Uri.parse(DynamicLink), + androidParameters: const AndroidParameters( + packageName: 'io.flutter.plugins.firebase.dynamiclinksexample', + minimumVersion: 0, + ), + iosParameters: const IOSParameters( + bundleId: 'io.invertase.testing', + minimumVersion: '0', + ), + ); + + Uri url; + if (short) { + final ShortDynamicLink shortLink = + await dynamicLinks.buildShortLink(parameters); + url = shortLink.shortUrl; + } else { + url = await dynamicLinks.buildLink(parameters); + } + + setState(() { + _linkMessage = url.toString(); + _isCreatingLink = false; + }); + } + + @override + Widget build(BuildContext context) { + return Material( + child: Scaffold( + appBar: AppBar( + title: const Text('Dynamic Links Example'), + ), + body: Builder( + builder: (BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ButtonBar( + alignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + final Uri? deepLink = data?.link; + + if (deepLink != null) { + // ignore: unawaited_futures + Navigator.pushNamed(context, deepLink.path); + } + }, + child: const Text('getInitialLink'), + ), + ElevatedButton( + onPressed: () async { + final PendingDynamicLinkData? data = + await dynamicLinks + .getDynamicLink(Uri.parse(Link)); + final Uri? deepLink = data?.link; + + if (deepLink != null) { + // ignore: unawaited_futures + Navigator.pushNamed(context, deepLink.path); + } + }, + child: const Text('getDynamicLink'), + ), + ElevatedButton( + onPressed: !_isCreatingLink + ? () => _createDynamicLink(false) + : null, + child: const Text('Get Long Link'), + ), + ElevatedButton( + onPressed: !_isCreatingLink + ? () => _createDynamicLink(true) + : null, + child: const Text('Get Short Link'), + ), + ], + ), + InkWell( + onTap: () async { + if (_linkMessage != null) { + await launch(_linkMessage!); + } + }, + onLongPress: () { + Clipboard.setData(ClipboardData(text: _linkMessage)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied Link!')), + ); + }, + child: Text( + _linkMessage ?? '', + style: const TextStyle(color: Colors.blue), + ), + ), + Text(_linkMessage == null ? '' : _testString) + ], + ), + ); + }, + ), + ), + ); + } +} + +class _DynamicLinkScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Material( + child: Scaffold( + appBar: AppBar( + title: const Text('Hello World DeepLink'), + ), + body: const Center( + child: Text('Hello, World!'), + ), + ), + ); + } +} diff --git a/packages/firebase_dynamic_links/example/pubspec.yaml b/packages/firebase_dynamic_links/firebase_dynamic_links/example/pubspec.yaml similarity index 59% rename from packages/firebase_dynamic_links/example/pubspec.yaml rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/pubspec.yaml index fcce1b116517..7537624239ba 100644 --- a/packages/firebase_dynamic_links/example/pubspec.yaml +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: firebase_core: - path: ../../firebase_core/firebase_core + path: ../../../firebase_core/firebase_core firebase_dynamic_links: path: ../ flutter: @@ -16,14 +16,16 @@ dependencies: dependency_overrides: firebase_core: - path: ../../firebase_core/firebase_core + path: ../../../firebase_core/firebase_core firebase_core_platform_interface: - path: ../../firebase_core/firebase_core_platform_interface - firebase_core_web: - path: ../../firebase_core/firebase_core_web + path: ../../../firebase_core/firebase_core_platform_interface + firebase_dynamic_links: + path: ../ + firebase_dynamic_links_platform_interface: + path: ../../firebase_dynamic_links_platform_interface dev_dependencies: - drive: 0.1.0 + drive: 1.0.0-1.0.nullsafety.1 flutter_driver: sdk: flutter flutter_test: diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart new file mode 100644 index 000000000000..2378d5af8168 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e.dart @@ -0,0 +1,29 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:drive/drive.dart' as drive; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'instance_e2e.dart'; + +void testsMain() { + setUpAll(() async { + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: 'AIzaSyCuu4tbv9CwwTudNOweMNstzZHIDBhgJxA', + appId: '1:448618578101:ios:4cd06f56e36384acac3efc', + messagingSenderId: '448618578101', + projectId: 'react-native-firebase-testing', + authDomain: 'react-native-firebase-testing.firebaseapp.com', + iosClientId: + '448618578101-m53gtqfnqipj12pts10590l37npccd2r.apps.googleusercontent.com', + ), + ); + }); + + runInstanceTests(); +} + +void main() => drive.main(testsMain); diff --git a/packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart similarity index 65% rename from packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart rename to packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart index 9ea0d073c35e..bb4e596d9ec0 100644 --- a/packages/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/firebase_dynamic_links_e2e_test.dart @@ -1,6 +1,4 @@ -// ignore_for_file: require_trailing_commas -// @dart = 2.9 -// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/instance_e2e.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/instance_e2e.dart new file mode 100644 index 000000000000..44a8a870700b --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/example/test_driver/instance_e2e.dart @@ -0,0 +1,170 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runInstanceTests() { + group('$FirebaseDynamicLinks', () { + late FirebaseDynamicLinks dynamicLinks; + + setUpAll(() async { + dynamicLinks = FirebaseDynamicLinks.instance; + }); + + group('instance', () { + test('instance', () { + expect(dynamicLinks, isA()); + expect(dynamicLinks.app, isA()); + }); + }); + + group('buildLink', () { + test('build normal dynamic links', () async { + FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance; + const String androidPackageName = + 'io.flutter.plugins.firebase.dynamiclinksexample'; + const String iosBundleId = + 'com.google.FirebaseCppDynamicLinksTestApp.dev'; + const String urlHost = 'reactnativefirebase.page.link'; + const String link = 'https://invertase.io'; + + final DynamicLinkParameters parameters = DynamicLinkParameters( + uriPrefix: 'https://$urlHost', + link: Uri.parse(link), + androidParameters: const AndroidParameters( + packageName: androidPackageName, + minimumVersion: 1, + ), + iosParameters: const IOSParameters( + bundleId: iosBundleId, + minimumVersion: '2', + ), + ); + + final Uri uri = await dynamicLinks.buildLink(parameters); + + // androidParameters.minimumVersion + expect( + uri.queryParameters['amv'], + '1', + ); + // iosParameters.minimumVersion + expect( + uri.queryParameters['imv'], + '2', + ); + // androidParameters.packageName + expect( + uri.queryParameters['apn'], + androidPackageName, + ); + // iosParameters.bundleId + expect( + uri.queryParameters['ibi'], + iosBundleId, + ); + // link + expect( + uri.queryParameters['link'], + Uri.encodeFull(link), + ); + // uriPrefix + expect( + uri.host, + urlHost, + ); + }); + }); + + group('buildShortLink', () { + test('build a short dynamic link', () async { + FirebaseDynamicLinks dynamicLinks = FirebaseDynamicLinks.instance; + const String androidPackageName = + 'io.flutter.plugins.firebase.dynamiclinksexample'; + const String iosBundleId = + 'io.flutter.plugins.firebase.dynamiclinksexample'; + const String urlHost = 'reactnativefirebase.page.link'; + const String link = 'https://invertase.io'; + + final DynamicLinkParameters parameters = DynamicLinkParameters( + uriPrefix: 'https://$urlHost', + link: Uri.parse(link), + androidParameters: const AndroidParameters( + packageName: androidPackageName, + minimumVersion: 1, + ), + iosParameters: const IOSParameters( + bundleId: iosBundleId, + minimumVersion: '2', + ), + ); + + final ShortDynamicLink uri = + await dynamicLinks.buildShortLink(parameters); + + // androidParameters.minimumVersion + expect( + uri.shortUrl.host, + urlHost, + ); + + expect( + uri.shortUrl.pathSegments.length, + equals(1), + ); + + expect( + uri.shortUrl.path.length, + lessThanOrEqualTo(18), + ); + }); + }); + + group('getInitialLink', () { + test('initial link', () async { + PendingDynamicLinkData? pendingLink = + await FirebaseDynamicLinks.instance.getInitialLink(); + + expect(pendingLink, isNull); + }); + }); + }); + + group('getDynamicLink', () { + test('dynamic link using uri', () async { + Uri uri = Uri.parse(''); + PendingDynamicLinkData? pendingLink = + await FirebaseDynamicLinks.instance.getDynamicLink(uri); + + expect(pendingLink, isNull); + }); + }); + + group('onLink', () { + test('test multiple times', () async { + StreamSubscription _onListenSubscription; + StreamSubscription _onListenSubscriptionSecond; + + _onListenSubscription = + FirebaseDynamicLinks.instance.onLink.listen((event) {}); + _onListenSubscriptionSecond = + FirebaseDynamicLinks.instance.onLink.listen((event) {}); + + await _onListenSubscription.cancel(); + await _onListenSubscriptionSecond.cancel(); + + _onListenSubscription = + FirebaseDynamicLinks.instance.onLink.listen((event) {}); + _onListenSubscriptionSecond = + FirebaseDynamicLinks.instance.onLink.listen((event) {}); + + await _onListenSubscription.cancel(); + await _onListenSubscriptionSecond.cancel(); + }); + }); +} diff --git a/packages/firebase_dynamic_links/ios/Assets/.gitkeep b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Assets/.gitkeep similarity index 100% rename from packages/firebase_dynamic_links/ios/Assets/.gitkeep rename to packages/firebase_dynamic_links/firebase_dynamic_links/ios/Assets/.gitkeep diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h new file mode 100644 index 000000000000..749b1fc6004a --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h @@ -0,0 +1,16 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#import +#import +#import + +@interface FLTFirebaseDynamicLinksPlugin : FLTFirebasePlugin + +@property(nonatomic, retain) NSError *initialError; +@property(nonatomic, retain) NSObject *messenger; +@property(nonatomic, retain) FlutterMethodChannel *channel; +@property(nonatomic, retain) FIRDynamicLink *initialLink; +@property(nonatomic, retain) FIRDynamicLink *latestLink; +@property(nonatomic) BOOL initiated; +@end diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m new file mode 100644 index 000000000000..4b996a441533 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m @@ -0,0 +1,473 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#import +#import +#import + +#import "FLTFirebaseDynamicLinksPlugin.h" + +NSString *const kFLTFirebaseDynamicLinksChannelName = @"plugins.flutter.io/firebase_dynamic_links"; +NSString *const kDLAppName = @"appName"; +NSString *const kUrl = @"url"; +NSString *const kCode = @"code"; +NSString *const kMessage = @"message"; +NSString *const kDynamicLinkParametersOptions = @"dynamicLinkParametersOptions"; +NSString *const kDefaultAppName = @"[DEFAULT]"; + +static NSMutableDictionary *getDictionaryFromDynamicLink(FIRDynamicLink *dynamicLink) { + if (dynamicLink != nil) { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + dictionary[@"link"] = dynamicLink.url.absoluteString; + + NSMutableDictionary *iosData = [[NSMutableDictionary alloc] init]; + if (dynamicLink.minimumAppVersion) { + iosData[@"minimumVersion"] = dynamicLink.minimumAppVersion; + } + dictionary[@"ios"] = iosData; + return dictionary; + } else { + return nil; + } +} + +static NSDictionary *getDictionaryFromNSError(NSError *error) { + NSString *code = @"unknown"; + NSString *message = @"An unknown error has occurred."; + if (error == nil) { + return @{ + kCode : code, + kMessage : message, + @"additionalData" : @{}, + }; + } + + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + dictionary[kCode] = [NSString stringWithFormat:@"%d", (int)error.code]; + dictionary[kMessage] = [error localizedDescription]; + id additionalData = [NSMutableDictionary dictionary]; + + if ([error userInfo] != nil) { + additionalData = [error userInfo]; + } + + return @{ + kCode : code, + kMessage : message, + @"additionalData" : additionalData, + }; +} + +@implementation FLTFirebaseDynamicLinksPlugin { + NSObject *_binaryMessenger; +} + +#pragma mark - FlutterPlugin + +- (instancetype)init:(NSObject *)messenger + withChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + [[FLTFirebasePluginRegistry sharedInstance] registerFirebasePlugin:self]; + _binaryMessenger = messenger; + _channel = channel; + } + return self; +} ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:kFLTFirebaseDynamicLinksChannelName + binaryMessenger:[registrar messenger]]; + FLTFirebaseDynamicLinksPlugin *instance = + [[FLTFirebaseDynamicLinksPlugin alloc] init:registrar.messenger withChannel:channel]; + + [registrar addMethodCallDelegate:instance channel:channel]; + +#if TARGET_OS_OSX + // Publish does not exist on MacOS version of FlutterPluginRegistrar. + // FlutterPluginRegistrar. (https://github.com/flutter/flutter/issues/41471) +#else + [registrar publish:instance]; + [registrar addApplicationDelegate:instance]; +#endif +} + +- (void)cleanupWithCompletion:(void (^)(void))completion { + if (completion != nil) completion(); +} + +- (void)detachFromEngineForRegistrar:(NSObject *)registrar { + [self cleanupWithCompletion:nil]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + FLTFirebaseMethodCallErrorBlock errorBlock = ^( + NSString *_Nullable code, NSString *_Nullable message, NSDictionary *_Nullable details, + NSError *_Nullable error) { + if (code == nil) { + NSDictionary *errorDetails = getDictionaryFromNSError(error); + code = errorDetails[kCode]; + message = errorDetails[kMessage]; + details = errorDetails; + } else { + details = @{ + kCode : code, + kMessage : message, + @"additionalData" : @{}, + }; + } + + if ([@"unknown" isEqualToString:code]) { + NSLog(@"FLTFirebaseDynamicLinks: An error occurred while calling method %@, errorOrNil => %@", + call.method, [error userInfo]); + } + + result([FLTFirebasePlugin createFlutterErrorFromCode:code + message:message + optionalDetails:details + andOptionalNSError:error]); + }; + + FLTFirebaseMethodCallResult *methodCallResult = + [FLTFirebaseMethodCallResult createWithSuccess:result andErrorBlock:errorBlock]; + + NSString *appName = call.arguments[kDLAppName]; + if (appName != nil && ![appName isEqualToString:kDefaultAppName]) { + // TODO - document iOS default app only + NSLog(@"FLTFirebaseDynamicLinks: iOS plugin only supports the Firebase default app"); + } + + if ([@"FirebaseDynamicLinks#buildLink" isEqualToString:call.method]) { + [self buildLink:call.arguments withMethodCallResult:methodCallResult]; + } else if ([@"FirebaseDynamicLinks#buildShortLink" isEqualToString:call.method]) { + [self buildShortLink:call.arguments withMethodCallResult:methodCallResult]; + } else if ([@"FirebaseDynamicLinks#getInitialLink" isEqualToString:call.method]) { + [self getInitialLink:methodCallResult]; + } else if ([@"FirebaseDynamicLinks#getDynamicLink" isEqualToString:call.method]) { + [self getDynamicLink:call.arguments withMethodCallResult:methodCallResult]; + } else { + result(FlutterMethodNotImplemented); + } +} + +#pragma mark - Firebase Dynamic Links API + +- (void)buildLink:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDynamicLinkComponents *components = [self setupParameters:arguments]; + result.success([components.url absoluteString]); +} + +- (void)buildShortLink:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + FIRDynamicLinkComponents *components = [self setupParameters:arguments]; + + [components + shortenWithCompletion:^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, + NSError *_Nullable error) { + if (error != nil) { + result.error(nil, nil, nil, error); + } else { + if (warnings == nil) { + warnings = [NSMutableArray array]; + } + + result.success(@{ + kUrl : [shortURL absoluteString], + @"warnings" : warnings, + }); + } + }]; +} + +- (void)getInitialLink:(FLTFirebaseMethodCallResult *)result { + _initiated = YES; + NSMutableDictionary *dict = getDictionaryFromDynamicLink(_initialLink); + if (dict == nil && self.initialError != nil) { + result.error(nil, nil, nil, self.initialError); + } else { + result.success(dict); + } +} + +- (void)getDynamicLink:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result { + NSURL *shortLink = [NSURL URLWithString:arguments[kUrl]]; + FIRDynamicLinkUniversalLinkHandler completion = + ^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (error) { + result.error(nil, nil, nil, error); + } else { + result.success(getDictionaryFromDynamicLink(dynamicLink)); + } + }; + [[FIRDynamicLinks dynamicLinks] handleUniversalLink:shortLink completion:completion]; +} + +#pragma mark - AppDelegate +// Handle links received through your app's custom URL scheme. Called when your +// app receives a link and your app is opened for the first time after installation. +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { + [self checkForDynamicLink:url]; + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. + return NO; +} + +// Handle links received as Universal Links when the app is already installed (on iOS 9 and newer). +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(nonnull void (^)(NSArray *_Nullable))restorationHandler { + __block BOOL retried = NO; + void (^completionBlock)(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error); + + void (^__block __weak weakCompletionBlock)(FIRDynamicLink *_Nullable dynamicLink, + NSError *_Nullable error); + weakCompletionBlock = completionBlock = + ^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (!error && dynamicLink && dynamicLink.url) { + [self onDeepLinkResult:dynamicLink error:nil]; + } + + if (!error && dynamicLink && !dynamicLink.url) { + NSLog(@"FLTFirebaseDynamicLinks: The url has not been supplied with the dynamic link." + @"Please try opening your app with the long dynamic link to see if that works"); + } + // Per Apple Tech Support, a network failure could occur when returning from background on + // iOS 12. https://github.com/AFNetworking/AFNetworking/issues/4279#issuecomment-447108981 + // So we'll retry the request once + if (error && !retried && [NSPOSIXErrorDomain isEqualToString:error.domain] && + error.code == 53) { + retried = YES; + [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL + completion:weakCompletionBlock]; + } + + if (error && retried) { + // Need to update any event channel the universal link failed + [self onDeepLinkResult:nil error:error]; + } + }; + + [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL + completion:completionBlock]; + + // Results of this are ORed and NO doesn't affect other delegate interceptors' result. + return NO; +} + +#pragma mark - Utilities + +- (void)checkForDynamicLink:(NSURL *)url { + FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; + if (dynamicLink) { + [self onDeepLinkResult:dynamicLink error:nil]; + } +} + +// Used to action events from firebase-ios-sdk custom & universal dynamic link event listeners +- (void)onDeepLinkResult:(FIRDynamicLink *_Nullable)dynamicLink error:(NSError *_Nullable)error { + if (error) { + if (_initialLink == nil) { + // store initial error to pass back to user if getInitialLink is called + _initialError = error; + } + + NSDictionary *errorDetails = getDictionaryFromNSError(error); + + FlutterError *flutterError = + [FLTFirebasePlugin createFlutterErrorFromCode:errorDetails[kCode] + message:errorDetails[kMessage] + optionalDetails:errorDetails + andOptionalNSError:error]; + + NSLog(@"FLTFirebaseDynamicLinks: Unknown error occurred when attempting to handle a dynamic " + @"link: %@", + flutterError); + + [_channel invokeMethod:@"FirebaseDynamicLink#onLinkError" arguments:flutterError]; + } else { + NSMutableDictionary *dictionary = getDictionaryFromDynamicLink(dynamicLink); + if (dictionary != nil) { + [_channel invokeMethod:@"FirebaseDynamicLink#onLinkSuccess" arguments:dictionary]; + } + } + + if (_initialLink == nil && dynamicLink.url != nil) { + _initialLink = dynamicLink; + } + + if (dynamicLink.url != nil) { + _latestLink = dynamicLink; + } +} + +- (FIRDynamicLinkComponentsOptions *)setupOptions:(NSDictionary *)arguments { + FIRDynamicLinkComponentsOptions *options = [FIRDynamicLinkComponentsOptions options]; + + NSNumber *shortDynamicLinkPathLength = arguments[@"shortLinkType"]; + if (![shortDynamicLinkPathLength isEqual:[NSNull null]]) { + switch (shortDynamicLinkPathLength.intValue) { + case 0: + options.pathLength = FIRShortDynamicLinkPathLengthUnguessable; + break; + case 1: + options.pathLength = FIRShortDynamicLinkPathLengthShort; + break; + default: + break; + } + } + + return options; +} + +- (FIRDynamicLinkComponents *)setupParameters:(NSDictionary *)arguments { + NSURL *link = [NSURL URLWithString:arguments[@"link"]]; + NSString *uriPrefix = arguments[@"uriPrefix"]; + + FIRDynamicLinkComponents *components = [FIRDynamicLinkComponents componentsWithLink:link + domainURIPrefix:uriPrefix]; + + if (![arguments[@"androidParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"androidParameters"]; + + FIRDynamicLinkAndroidParameters *androidParams = + [FIRDynamicLinkAndroidParameters parametersWithPackageName:params[@"packageName"]]; + + NSString *fallbackUrl = params[@"fallbackUrl"]; + NSNumber *minimumVersion = params[@"minimumVersion"]; + + if (![fallbackUrl isEqual:[NSNull null]]) + androidParams.fallbackURL = [NSURL URLWithString:fallbackUrl]; + if (![minimumVersion isEqual:[NSNull null]]) + androidParams.minimumVersion = ((NSNumber *)minimumVersion).integerValue; + + components.androidParameters = androidParams; + } + + components.options = [self setupOptions:arguments]; + + if (![arguments[@"googleAnalyticsParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"googleAnalyticsParameters"]; + + FIRDynamicLinkGoogleAnalyticsParameters *googleAnalyticsParameters = + [FIRDynamicLinkGoogleAnalyticsParameters parameters]; + + NSString *campaign = params[@"campaign"]; + NSString *content = params[@"content"]; + NSString *medium = params[@"medium"]; + NSString *source = params[@"source"]; + NSString *term = params[@"term"]; + + if (![campaign isEqual:[NSNull null]]) googleAnalyticsParameters.campaign = campaign; + if (![content isEqual:[NSNull null]]) googleAnalyticsParameters.content = content; + if (![medium isEqual:[NSNull null]]) googleAnalyticsParameters.medium = medium; + if (![source isEqual:[NSNull null]]) googleAnalyticsParameters.source = source; + if (![term isEqual:[NSNull null]]) googleAnalyticsParameters.term = term; + + components.analyticsParameters = googleAnalyticsParameters; + } + + if (![arguments[@"iosParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"iosParameters"]; + + FIRDynamicLinkIOSParameters *iosParameters = + [FIRDynamicLinkIOSParameters parametersWithBundleID:params[@"bundleId"]]; + + NSString *appStoreID = params[@"appStoreId"]; + NSString *customScheme = params[@"customScheme"]; + NSString *fallbackURL = params[@"fallbackUrl"]; + NSString *iPadBundleID = params[@"ipadBundleId"]; + NSString *iPadFallbackURL = params[@"ipadFallbackUrl"]; + NSString *minimumAppVersion = params[@"minimumVersion"]; + + if (![appStoreID isEqual:[NSNull null]]) iosParameters.appStoreID = appStoreID; + if (![customScheme isEqual:[NSNull null]]) iosParameters.customScheme = customScheme; + if (![fallbackURL isEqual:[NSNull null]]) + iosParameters.fallbackURL = [NSURL URLWithString:fallbackURL]; + if (![iPadBundleID isEqual:[NSNull null]]) iosParameters.iPadBundleID = iPadBundleID; + if (![iPadFallbackURL isEqual:[NSNull null]]) + iosParameters.iPadFallbackURL = [NSURL URLWithString:iPadFallbackURL]; + if (![minimumAppVersion isEqual:[NSNull null]]) + iosParameters.minimumAppVersion = minimumAppVersion; + + components.iOSParameters = iosParameters; + } + + if (![arguments[@"itunesConnectAnalyticsParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"itunesConnectAnalyticsParameters"]; + + FIRDynamicLinkItunesConnectAnalyticsParameters *itunesConnectAnalyticsParameters = + [FIRDynamicLinkItunesConnectAnalyticsParameters parameters]; + + NSString *affiliateToken = params[@"affiliateToken"]; + NSString *campaignToken = params[@"campaignToken"]; + NSString *providerToken = params[@"providerToken"]; + + if (![affiliateToken isEqual:[NSNull null]]) + itunesConnectAnalyticsParameters.affiliateToken = affiliateToken; + if (![campaignToken isEqual:[NSNull null]]) + itunesConnectAnalyticsParameters.campaignToken = campaignToken; + if (![providerToken isEqual:[NSNull null]]) + itunesConnectAnalyticsParameters.providerToken = providerToken; + + components.iTunesConnectParameters = itunesConnectAnalyticsParameters; + } + + if (![arguments[@"navigationInfoParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"navigationInfoParameters"]; + + FIRDynamicLinkNavigationInfoParameters *navigationInfoParameters = + [FIRDynamicLinkNavigationInfoParameters parameters]; + + NSNumber *forcedRedirectEnabled = params[@"forcedRedirectEnabled"]; + if (![forcedRedirectEnabled isEqual:[NSNull null]]) + navigationInfoParameters.forcedRedirectEnabled = [forcedRedirectEnabled boolValue]; + + components.navigationInfoParameters = navigationInfoParameters; + } + + if (![arguments[@"socialMetaTagParameters"] isEqual:[NSNull null]]) { + NSDictionary *params = arguments[@"socialMetaTagParameters"]; + + FIRDynamicLinkSocialMetaTagParameters *socialMetaTagParameters = + [FIRDynamicLinkSocialMetaTagParameters parameters]; + + NSString *descriptionText = params[@"description"]; + NSString *imageURL = params[@"imageUrl"]; + NSString *title = params[@"title"]; + + if (![descriptionText isEqual:[NSNull null]]) + socialMetaTagParameters.descriptionText = descriptionText; + if (![imageURL isEqual:[NSNull null]]) + socialMetaTagParameters.imageURL = [NSURL URLWithString:imageURL]; + if (![title isEqual:[NSNull null]]) socialMetaTagParameters.title = title; + + components.socialMetaTagParameters = socialMetaTagParameters; + } + + return components; +} + +#pragma mark - FLTFirebasePlugin + +- (void)didReinitializeFirebaseCore:(void (^)(void))completion { + [self cleanupWithCompletion:completion]; +} + +- (NSDictionary *_Nonnull)pluginConstantsForFIRApp:(FIRApp *)firebase_app { + return @{}; +} + +- (NSString *_Nonnull)firebaseLibraryName { + return LIBRARY_NAME; +} + +- (NSString *_Nonnull)firebaseLibraryVersion { + return LIBRARY_VERSION; +} + +- (NSString *_Nonnull)flutterChannelName { + return kFLTFirebaseDynamicLinksChannelName; +} + +@end diff --git a/packages/firebase_dynamic_links/ios/firebase_dynamic_links.podspec b/packages/firebase_dynamic_links/firebase_dynamic_links/ios/firebase_dynamic_links.podspec similarity index 100% rename from packages/firebase_dynamic_links/ios/firebase_dynamic_links.podspec rename to packages/firebase_dynamic_links/firebase_dynamic_links/ios/firebase_dynamic_links.podspec diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/lib/firebase_dynamic_links.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/lib/firebase_dynamic_links.dart new file mode 100644 index 000000000000..0506df45faaf --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/lib/firebase_dynamic_links.dart @@ -0,0 +1,29 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library firebase_dynamic_links; + +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +export 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart' + show + AndroidParameters, + DynamicLinkParameters, + FirebaseDynamicLinksPlatform, + GoogleAnalyticsParameters, + IOSParameters, + ITunesConnectAnalyticsParameters, + NavigationInfoParameters, + PendingDynamicLinkData, + PendingDynamicLinkDataAndroid, + PendingDynamicLinkDataIOS, + ShortDynamicLink, + ShortDynamicLinkType, + SocialMetaTagParameters; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +part 'src/firebase_dynamic_links.dart'; diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart new file mode 100644 index 000000000000..034fea588e0e --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart @@ -0,0 +1,87 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of firebase_dynamic_links; + +/// Firebase Dynamic Links API. +/// +/// You can get an instance by calling [FirebaseDynamicLinks.instance]. +class FirebaseDynamicLinks extends FirebasePluginPlatform { + FirebaseDynamicLinks._({required this.app}) + : super(app.name, 'plugins.flutter.io/firebase_dynamic_links'); + + static final Map _cachedInstances = {}; + + /// Returns an instance using the default [FirebaseApp]. + static FirebaseDynamicLinks get instance { + return FirebaseDynamicLinks.instanceFor( + app: Firebase.app(), + ); + } + + /// Returns an instance using a specified [FirebaseApp]. + /// Note; multi-app support is only supported on android. + static FirebaseDynamicLinks instanceFor({required FirebaseApp app}) { + if (defaultTargetPlatform == TargetPlatform.android || + app.name == defaultFirebaseAppName) { + return _cachedInstances.putIfAbsent(app.name, () { + return FirebaseDynamicLinks._(app: app); + }); + } + + throw UnsupportedError( + 'FirebaseDynamicLinks.instanceFor() only supports non-default FirebaseApp instances on Android.', + ); + } + + // Cached and lazily loaded instance of [FirebaseDynamicLinksPlatform] to avoid + // creating a [MethodChannelFirebaseDynamicLinks] when not needed or creating an + // instance with the default app before a user specifies an app. + FirebaseDynamicLinksPlatform? _delegatePackingProperty; + + FirebaseDynamicLinksPlatform get _delegate { + return _delegatePackingProperty ??= + FirebaseDynamicLinksPlatform.instanceFor(app: app); + } + + /// The [FirebaseApp] for this current [FirebaseDynamicLinks] instance. + FirebaseApp app; + + /// Attempts to retrieve the dynamic link which launched the app. + /// + /// This method always returns a Future. That Future completes to null if + /// there is no pending dynamic link or any call to this method after the + /// the first attempt. + Future getInitialLink() async { + return _delegate.getInitialLink(); + } + + /// Determine if the app has a pending dynamic link and provide access to + /// the dynamic link parameters. A pending dynamic link may have been + /// previously captured when a user clicked on a dynamic link, or + /// may be present in the dynamicLinkUri parameter. If both are present, + /// the previously captured dynamic link will take precedence. The captured + /// data will be removed after first access. + Future getDynamicLink(Uri url) async { + return _delegate.getDynamicLink(url); + } + + /// Listen to a stream for the latest dynamic link events. + Stream get onLink { + return _delegate.onLink; + } + + /// Creates a Dynamic Link from the parameters. + Future buildLink(DynamicLinkParameters parameters) async { + return _delegate.buildLink(parameters); + } + + /// Creates a shortened Dynamic Link from the parameters. + Future buildShortLink( + DynamicLinkParameters parameters, { + ShortDynamicLinkType shortLinkType = ShortDynamicLinkType.short, + }) async { + return _delegate.buildShortLink(parameters, shortLinkType: shortLinkType); + } +} diff --git a/packages/firebase_dynamic_links/pubspec.yaml b/packages/firebase_dynamic_links/firebase_dynamic_links/pubspec.yaml similarity index 72% rename from packages/firebase_dynamic_links/pubspec.yaml rename to packages/firebase_dynamic_links/firebase_dynamic_links/pubspec.yaml index 7c6ecd5eee09..96f7109a6d97 100644 --- a/packages/firebase_dynamic_links/pubspec.yaml +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/pubspec.yaml @@ -12,19 +12,24 @@ environment: dependencies: firebase_core: ^1.10.0 + firebase_core_platform_interface: ^4.0.1 + firebase_dynamic_links_platform_interface: ^0.1.0 flutter: sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.2 dev_dependencies: flutter_test: sdk: flutter + mockito: ^5.0.0 url_launcher: ^6.0.2 flutter: plugin: platforms: android: - package: io.flutter.plugins.firebasedynamiclinks - pluginClass: FirebaseDynamicLinksPlugin + package: io.flutter.plugins.firebase.dynamiclinks + pluginClass: FlutterFirebaseDynamicLinksPlugin ios: pluginClass: FLTFirebaseDynamicLinksPlugin diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/test/firebase_dynamic_links_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/test/firebase_dynamic_links_test.dart new file mode 100644 index 000000000000..4c05fb0a33e3 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/test/firebase_dynamic_links_test.dart @@ -0,0 +1,401 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import './mock.dart'; + +MockFirebaseDynamicLinks mockDynamicLinksPlatform = MockFirebaseDynamicLinks(); + +DynamicLinkParameters buildDynamicLinkParameters() { + AndroidParameters android = AndroidParameters( + fallbackUrl: Uri.parse('test-url'), + minimumVersion: 1, + packageName: 'test-package', + ); + + GoogleAnalyticsParameters google = const GoogleAnalyticsParameters( + campaign: 'campaign', + medium: 'medium', + source: 'source', + term: 'term', + content: 'content', + ); + + IOSParameters ios = IOSParameters( + appStoreId: 'appStoreId', + bundleId: 'bundleId', + customScheme: 'customScheme', + fallbackUrl: Uri.parse('fallbackUrl'), + ipadBundleId: 'ipadBundleId', + ipadFallbackUrl: Uri.parse('ipadFallbackUrl'), + minimumVersion: 'minimumVersion', + ); + + ITunesConnectAnalyticsParameters itunes = + const ITunesConnectAnalyticsParameters( + affiliateToken: 'affiliateToken', + campaignToken: 'campaignToken', + providerToken: 'providerToken', + ); + + Uri link = Uri.parse('link'); + NavigationInfoParameters navigation = + const NavigationInfoParameters(forcedRedirectEnabled: true); + SocialMetaTagParameters social = SocialMetaTagParameters( + description: 'description', + imageUrl: Uri.parse('imageUrl'), + title: 'title', + ); + + String uriPrefix = 'https://'; + + return DynamicLinkParameters( + uriPrefix: uriPrefix, + link: link, + androidParameters: android, + googleAnalyticsParameters: google, + iosParameters: ios, + itunesConnectAnalyticsParameters: itunes, + navigationInfoParameters: navigation, + socialMetaTagParameters: social, + ); +} + +void main() { + setupFirebaseDynamicLinksMocks(); + + late FirebaseDynamicLinks dynamicLinks; + + group('$FirebaseDynamicLinks', () { + setUpAll(() async { + FirebaseDynamicLinksPlatform.instance = mockDynamicLinksPlatform; + + await Firebase.initializeApp(); + + dynamicLinks = FirebaseDynamicLinks.instance; + }); + + group('getInitialLink', () { + test('link can be parsed', () async { + const mockClickTimestamp = 1234567; + const mockMinimumVersionAndroid = 12; + const mockMinimumVersionIOS = 'ios minimum version'; + Uri mockUri = Uri.parse('mock-scheme'); + + when(dynamicLinks.getInitialLink()).thenAnswer( + (_) async => TestPendingDynamicLinkData( + mockUri, + mockClickTimestamp, + mockMinimumVersionAndroid, + mockMinimumVersionIOS, + ), + ); + + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + + expect(data!.link.scheme, mockUri.scheme); + + expect(data.android!.clickTimestamp, mockClickTimestamp); + expect(data.android!.minimumVersion, mockMinimumVersionAndroid); + + expect(data.ios!.minimumVersion, mockMinimumVersionIOS); + + verify(dynamicLinks.getInitialLink()); + }); + + test('for null result, returns null', () async { + when(dynamicLinks.getInitialLink()).thenAnswer((_) async => null); + + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + + expect(data, isNull); + + verify(dynamicLinks.getInitialLink()); + }); + }); + + group('getDynamicLink', () { + test('getDynamicLink', () async { + final Uri mockUri = Uri.parse('short-link'); + const mockClickTimestamp = 38947390875; + const mockMinimumVersionAndroid = 21; + const mockMinimumVersionIOS = 'min version'; + + when(dynamicLinks.getDynamicLink(mockUri)).thenAnswer( + (_) async => TestPendingDynamicLinkData( + mockUri, + mockClickTimestamp, + mockMinimumVersionAndroid, + mockMinimumVersionIOS, + ), + ); + + final PendingDynamicLinkData? data = + await dynamicLinks.getDynamicLink(mockUri); + + expect(data!.link.scheme, mockUri.scheme); + + expect(data.android!.clickTimestamp, mockClickTimestamp); + expect(data.android!.minimumVersion, mockMinimumVersionAndroid); + + expect(data.ios!.minimumVersion, mockMinimumVersionIOS); + + verify(dynamicLinks.getDynamicLink(mockUri)); + }); + }); + + group('onLink', () { + test('onLink', () async { + final Uri mockUri = Uri.parse('on-link'); + const mockClickTimestamp = 239058435; + const mockMinimumVersionAndroid = 33; + const mockMinimumVersionIOS = 'on-link version'; + when(dynamicLinks.onLink).thenAnswer( + (_) => Stream.value( + TestPendingDynamicLinkData( + mockUri, + mockClickTimestamp, + mockMinimumVersionAndroid, + mockMinimumVersionIOS, + ), + ), + ); + + final PendingDynamicLinkData data = await dynamicLinks.onLink.first; + expect(data.link.scheme, mockUri.scheme); + + expect(data.android!.clickTimestamp, mockClickTimestamp); + expect(data.android!.minimumVersion, mockMinimumVersionAndroid); + + expect(data.ios!.minimumVersion, mockMinimumVersionIOS); + + verify(dynamicLinks.onLink); + }); + }); + + group('buildLink', () { + test('buildLink', () async { + final Uri mockUri = Uri.parse('buildLink'); + DynamicLinkParameters params = + DynamicLinkParameters(uriPrefix: 'uriPrefix', link: mockUri); + + when(dynamicLinks.buildLink(params)).thenAnswer((_) async => mockUri); + + final shortDynamicLink = await dynamicLinks.buildLink(params); + + expect(shortDynamicLink, mockUri); + expect(shortDynamicLink.scheme, mockUri.scheme); + expect(shortDynamicLink.path, mockUri.path); + + verify(dynamicLinks.buildLink(params)); + }); + + test("buildLink with full 'DynamicLinkParameters' options", () async { + final Uri mockUri = Uri.parse('buildLink'); + DynamicLinkParameters params = buildDynamicLinkParameters(); + + when(dynamicLinks.buildLink(params)).thenAnswer((_) async => mockUri); + + final shortDynamicLink = await dynamicLinks.buildLink(params); + + expect(shortDynamicLink, mockUri); + expect(shortDynamicLink.scheme, mockUri.scheme); + expect(shortDynamicLink.path, mockUri.path); + + verify(dynamicLinks.buildLink(params)); + }); + }); + + group('buildShortLink', () { + test('buildShortLink', () async { + final Uri mockUri = Uri.parse('buildShortLink'); + final Uri previewLink = Uri.parse('previewLink'); + List warnings = ['warning']; + DynamicLinkParameters params = + DynamicLinkParameters(uriPrefix: 'uriPrefix', link: mockUri); + final shortLink = ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: mockUri, + warnings: warnings, + previewLink: previewLink, + ); + + when(dynamicLinks.buildShortLink(params)).thenAnswer( + (_) async => ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: mockUri, + warnings: warnings, + previewLink: previewLink, + ), + ); + + final shortDynamicLink = await dynamicLinks.buildShortLink(params); + + expect(shortDynamicLink.warnings, shortLink.warnings); + expect(shortDynamicLink.shortUrl, shortLink.shortUrl); + expect(shortDynamicLink.previewLink, shortLink.previewLink); + + verify(dynamicLinks.buildShortLink(params)); + }); + + test("buildShortLink with full 'DynamicLinkParameters' options", + () async { + final Uri mockUri = Uri.parse('buildShortLink'); + final Uri previewLink = Uri.parse('previewLink'); + List warnings = ['warning']; + DynamicLinkParameters params = buildDynamicLinkParameters(); + final shortLink = ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: mockUri, + warnings: warnings, + previewLink: previewLink, + ); + + when(dynamicLinks.buildShortLink(params)).thenAnswer( + (_) async => ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: mockUri, + warnings: warnings, + previewLink: previewLink, + ), + ); + + final shortDynamicLink = await dynamicLinks.buildShortLink(params); + + expect(shortDynamicLink.warnings, shortLink.warnings); + expect(shortDynamicLink.shortUrl, shortLink.shortUrl); + expect(shortDynamicLink.previewLink, shortLink.previewLink); + + verify(dynamicLinks.buildShortLink(params)); + }); + }); + }); +} + +class TestPendingDynamicLinkData extends PendingDynamicLinkData { + TestPendingDynamicLinkData( + mockUri, + mockClickTimestamp, + mockMinimumVersionAndroid, + mockMinimumVersionIOS, + ) : super( + link: mockUri, + android: PendingDynamicLinkDataAndroid( + clickTimestamp: mockClickTimestamp, + minimumVersion: mockMinimumVersionAndroid, + ), + ios: PendingDynamicLinkDataIOS( + minimumVersion: mockMinimumVersionIOS, + ), + ); +} + +final testData = TestPendingDynamicLinkData(Uri.parse('uri'), null, null, null); + +Future testFutureData() { + return Future.value(testData); +} + +Uri uri = Uri.parse('mock'); + +class MockFirebaseDynamicLinks extends Mock + with + MockPlatformInterfaceMixin + implements +// ignore: avoid_implementing_value_types + TestFirebaseDynamicLinksPlatform { + @override + Future getInitialLink() { + return super.noSuchMethod( + Invocation.method(#getInitialLink, []), + returnValue: testFutureData(), + returnValueForMissingStub: testFutureData(), + ); + } + + @override + Future getDynamicLink(Uri uri) { + return super.noSuchMethod( + Invocation.method(#getDynamicLink, [], {#uri: uri}), + returnValue: testFutureData(), + returnValueForMissingStub: testFutureData(), + ); + } + + @override + Future buildLink(DynamicLinkParameters parameters) { + return super.noSuchMethod( + Invocation.method(#buildLink, [parameters]), + returnValue: Future.value(Uri.parse('buildLink')), + returnValueForMissingStub: Future.value(Uri.parse('buildLink')), + ); + } + + @override + FirebaseDynamicLinksPlatform delegateFor({required FirebaseApp app}) { + return super.noSuchMethod( + Invocation.method(#delegateFor, [], {#app: app}), + returnValue: MockFirebaseDynamicLinks(), + returnValueForMissingStub: MockFirebaseDynamicLinks(), + ); + } + + @override + Future buildShortLink( + DynamicLinkParameters parameters, { + ShortDynamicLinkType shortLinkType = ShortDynamicLinkType.short, + }) { + return super.noSuchMethod( + Invocation.method(#buildShortLink, [parameters]), + returnValue: Future.value( + ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: uri, + warnings: ['warning'], + previewLink: Uri.parse('preview'), + ), + ), + returnValueForMissingStub: Future.value( + ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: uri, + warnings: ['warning'], + previewLink: Uri.parse('preview'), + ), + ), + ); + } + + @override + Stream get onLink { + return super.noSuchMethod( + Invocation.getter(#onLink), + returnValue: Stream.value(testData), + returnValueForMissingStub: Stream.value(testData), + ); + } +} + +class TestFirebaseDynamicLinksPlatform extends FirebaseDynamicLinksPlatform { + TestFirebaseDynamicLinksPlatform() : super(); + + void instanceFor({ + FirebaseApp? app, + }) {} + + @override + FirebaseDynamicLinksPlatform delegateFor({required FirebaseApp app}) { + return this; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links/test/mock.dart b/packages/firebase_dynamic_links/firebase_dynamic_links/test/mock.dart new file mode 100644 index 000000000000..7b9b0b70feab --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links/test/mock.dart @@ -0,0 +1,52 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +typedef Callback = void Function(MethodCall call); + +void setupFirebaseDynamicLinksMocks([Callback? customHandlers]) { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelFirebase.channel.setMockMethodCallHandler((call) async { + if (call.method == 'Firebase#initializeCore') { + return [ + { + 'name': defaultFirebaseAppName, + 'options': { + 'apiKey': '123', + 'appId': '123', + 'messagingSenderId': '123', + 'projectId': '123', + }, + 'pluginConstants': {}, + } + ]; + } + + if (call.method == 'Firebase#initializeApp') { + return { + 'name': call.arguments['appName'], + 'options': call.arguments['options'], + 'pluginConstants': {}, + }; + } + + if (customHandlers != null) { + customHandlers(call); + } + + return null; + }); +} + +Future neverEndingFuture() async { + // ignore: literal_only_boolean_expressions + while (true) { + await Future.delayed(const Duration(minutes: 5)); + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/.gitignore b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/.gitignore new file mode 100644 index 000000000000..2f9ef791f160 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/.gitignore @@ -0,0 +1,76 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +.metadata + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/CHANGELOG.md b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..08378c690145 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0 + +- Initial release. diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/LICENSE b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/LICENSE new file mode 100644 index 000000000000..e8a438415fb0 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/LICENSE @@ -0,0 +1,26 @@ +Copyright 2021, the Chromium project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/README.md b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/README.md new file mode 100644 index 000000000000..b91dcd77d97e --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/README.md @@ -0,0 +1,26 @@ +# firebase_dynamic_links_platform_interface + +A common platform interface for the [`firebase_dynamic_links`][1] plugin. + +This interface allows platform-specific implementations of the `firebase_dynamic_links` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +## Usage + +To implement a new platform-specific implementation of `firebase_dynamic_links`, extend +[`FirebaseDynamicLinksPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`FirebaseDynamicLinksPlatform` by calling +`FirebaseDynamicLinksPlatform.instance = MyDynamicLinks()`. + +## Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../firebase_dynamic_links +[2]: lib/firebase_dynamic_links_platform_interface.dart diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/firebase_dynamic_links_platform_interface.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/firebase_dynamic_links_platform_interface.dart new file mode 100644 index 000000000000..20afa43504cb --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/firebase_dynamic_links_platform_interface.dart @@ -0,0 +1,19 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +library firebase_dynamic_links_platform_interface; + +export 'src/platform_interface/platform_interface_firebase_dynamic_links.dart'; +export 'src/pending_dynamic_link_data.dart'; +export 'src/pending_dynamic_link_data_android.dart'; +export 'src/pending_dynamic_link_data_ios.dart'; +export 'src/short_dynamic_link.dart'; +export 'src/short_dynamic_link_type.dart'; +export 'src/google_analytics_parameters.dart'; +export 'src/ios_parameters.dart'; +export 'src/itunes_connect_analytics_parameters.dart'; +export 'src/navigation_info_parameters.dart'; +export 'src/social_meta_tag_parameters.dart'; +export 'src/android_parameters.dart'; +export 'src/dynamic_link_parameters.dart'; diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/android_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/android_parameters.dart new file mode 100644 index 000000000000..96b0ccd24788 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/android_parameters.dart @@ -0,0 +1,39 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The Dynamic Link Android parameters. +class AndroidParameters { + const AndroidParameters({ + this.fallbackUrl, + this.minimumVersion, + required this.packageName, + }); + + /// The link to open when the app isn’t installed. + /// + /// Specify this to do something other than install the app from the Play + /// Store when the app isn’t installed, such as open the mobile web version of + /// the content, or display a promotional page for the app. + final Uri? fallbackUrl; + + /// The version of the minimum version of your app that can open the link. + /// + /// If the installed app is an older version, the user is taken to the Play + /// Store to upgrade the app. + final int? minimumVersion; + + /// The Android app’s package name. + final String packageName; + + Map asMap() => { + 'fallbackUrl': fallbackUrl?.toString(), + 'minimumVersion': minimumVersion, + 'packageName': packageName, + }; + + @override + String toString() { + return '$AndroidParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/dynamic_link_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/dynamic_link_parameters.dart new file mode 100644 index 000000000000..426ffd74ed1b --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/dynamic_link_parameters.dart @@ -0,0 +1,79 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../firebase_dynamic_links_platform_interface.dart'; + +/// Interface that defines the all the parameters required to build a dynamic link +class DynamicLinkParameters { + // ignore: public_member_api_docs + DynamicLinkParameters({ + required this.link, + required this.uriPrefix, + this.androidParameters, + this.iosParameters, + this.googleAnalyticsParameters, + this.itunesConnectAnalyticsParameters, + this.navigationInfoParameters, + this.socialMetaTagParameters, + }); + + /// Android parameters for a generated Dynamic Link URL. + final AndroidParameters? androidParameters; + + /// Domain URI Prefix of your App. + // This value must be your assigned domain from the Firebase console. + // (e.g. https://xyz.page.link) + // + // The domain URI prefix must start with a valid HTTPS scheme (https://). + final String uriPrefix; + + /// Analytics parameters for a generated Dynamic Link URL. + final GoogleAnalyticsParameters? googleAnalyticsParameters; + + /// iOS parameters for a generated Dynamic Link URL. + final IOSParameters? iosParameters; + + /// iTunes Connect parameters for a generated Dynamic Link URL. + final ITunesConnectAnalyticsParameters? itunesConnectAnalyticsParameters; + + /// The link the target app will open. + /// + /// You can specify any URL the app can handle, such as a link to the app’s + /// content, or a URL that initiates some app-specific logic such as crediting + /// the user with a coupon, or displaying a specific welcome screen. + /// This link must be a well-formatted URL, be properly URL-encoded, and use + /// the HTTP or HTTPS scheme. + final Uri link; + + /// Navigation Info parameters for a generated Dynamic Link URL. + final NavigationInfoParameters? navigationInfoParameters; + + /// Social Meta Tag parameters for a generated Dynamic Link URL. + final SocialMetaTagParameters? socialMetaTagParameters; + + /// Returns the current instance as a [Map]. + Map asMap() { + return { + 'uriPrefix': uriPrefix, + 'link': link.toString(), + if (androidParameters != null) + 'androidParameters': androidParameters?.asMap(), + if (googleAnalyticsParameters != null) + 'googleAnalyticsParameters': googleAnalyticsParameters?.asMap(), + if (iosParameters != null) 'iosParameters': iosParameters?.asMap(), + if (itunesConnectAnalyticsParameters != null) + 'itunesConnectAnalyticsParameters': + itunesConnectAnalyticsParameters?.asMap(), + if (navigationInfoParameters != null) + 'navigationInfoParameters': navigationInfoParameters?.asMap(), + if (socialMetaTagParameters != null) + 'socialMetaTagParameters': socialMetaTagParameters?.asMap(), + }; + } + + @override + String toString() { + return '$DynamicLinkParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/google_analytics_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/google_analytics_parameters.dart new file mode 100644 index 000000000000..3c4d34e0c78e --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/google_analytics_parameters.dart @@ -0,0 +1,42 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The Dynamic Link analytics parameters. +class GoogleAnalyticsParameters { + const GoogleAnalyticsParameters({ + this.campaign, + this.content, + this.medium, + this.source, + this.term, + }); + + /// The utm_campaign analytics parameter. + final String? campaign; + + /// The utm_content analytics parameter. + final String? content; + + /// The utm_medium analytics parameter. + final String? medium; + + /// The utm_source analytics parameter. + final String? source; + + /// The utm_term analytics parameter. + final String? term; + + Map asMap() => { + 'campaign': campaign, + 'content': content, + 'medium': medium, + 'source': source, + 'term': term, + }; + + @override + String toString() { + return '$GoogleAnalyticsParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/ios_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/ios_parameters.dart new file mode 100644 index 000000000000..e96f8b20ab8c --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/ios_parameters.dart @@ -0,0 +1,67 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The Dynamic Link iOS parameters. +class IOSParameters { + const IOSParameters({ + this.appStoreId, + required this.bundleId, + this.customScheme, + this.fallbackUrl, + this.ipadBundleId, + this.ipadFallbackUrl, + this.minimumVersion, + }); + + /// The appStore ID of the iOS app in AppStore. + final String? appStoreId; + + /// The bundle ID of the iOS app to use to open the link. + final String bundleId; + + /// The target app’s custom URL scheme. + /// + /// Defined to be something other than the app’s bundle ID. + final String? customScheme; + + /// The link to open when the app isn’t installed. + /// + /// Specify this to do something other than install the app from the App Store + /// when the app isn’t installed, such as open the mobile web version of the + /// content, or display a promotional page for the app. + final Uri? fallbackUrl; + + /// The bundle ID of the iOS app to use on iPads to open the link. + /// + /// This is only required if there are separate iPhone and iPad applications. + final String? ipadBundleId; + + /// The link to open on iPads when the app isn’t installed. + /// + /// Specify this to do something other than install the app from the App Store + /// when the app isn’t installed, such as open the web version of the content, + /// or display a promotional page for the app. + final Uri? ipadFallbackUrl; + + /// The the minimum version of your app that can open the link. + /// + /// It is app’s developer responsibility to open AppStore when received link + /// declares higher [minimumVersion] than currently installed. + final String? minimumVersion; + + Map asMap() => { + 'appStoreId': appStoreId, + 'bundleId': bundleId, + 'customScheme': customScheme, + 'fallbackUrl': fallbackUrl?.toString(), + 'ipadBundleId': ipadBundleId, + 'ipadFallbackUrl': ipadFallbackUrl?.toString(), + 'minimumVersion': minimumVersion, + }; + + @override + String toString() { + return '$IOSParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/itunes_connect_analytics_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/itunes_connect_analytics_parameters.dart new file mode 100644 index 000000000000..595319f63440 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/itunes_connect_analytics_parameters.dart @@ -0,0 +1,32 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The Dynamic Link iTunes Connect parameters. +class ITunesConnectAnalyticsParameters { + const ITunesConnectAnalyticsParameters({ + this.affiliateToken, + this.campaignToken, + this.providerToken, + }); + + /// The iTunes Connect affiliate token. + final String? affiliateToken; + + /// The iTunes Connect campaign token. + final String? campaignToken; + + /// The iTunes Connect provider token. + final String? providerToken; + + Map asMap() => { + 'affiliateToken': affiliateToken, + 'campaignToken': campaignToken, + 'providerToken': providerToken, + }; + + @override + String toString() { + return '$ITunesConnectAnalyticsParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/method_channel_firebase_dynamic_links.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/method_channel_firebase_dynamic_links.dart new file mode 100644 index 000000000000..240e2fdb77f6 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/method_channel_firebase_dynamic_links.dart @@ -0,0 +1,187 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; + +import 'utils/exception.dart'; + +/// The entry point for accessing a Dynamic Links instance. +/// +/// You can get an instance by calling [FirebaseDynamicLinks.instance]. +class MethodChannelFirebaseDynamicLinks extends FirebaseDynamicLinksPlatform { + /// Create an instance of [MethodChannelFirebaseDynamicLinks] with optional [FirebaseApp] + MethodChannelFirebaseDynamicLinks({FirebaseApp? app}) + : super(appInstance: app) { + if (_initialized) return; + + channel.setMethodCallHandler((MethodCall call) async { + switch (call.method) { + case 'FirebaseDynamicLink#onLinkSuccess': + Map event = + Map.from(call.arguments); + PendingDynamicLinkData? data = + _getPendingDynamicLinkDataFromMap(event); + + if (data != null) { + _onLinkController.add(data); + } + break; + case 'FirebaseDynamicLink#onLinkError': + Map error = + Map.from(call.arguments); + _onLinkController.addError(convertPlatformException(error)); + break; + default: + throw UnimplementedError('${call.method} has not been implemented'); + } + }); + _initialized = true; + } + + static bool _initialized = false; + + /// The [FirebaseApp] instance to which this [FirebaseDynamicLinks] belongs. + /// + /// If null, the default [FirebaseApp] is used. + + /// The [MethodChannel] used to communicate with the native plugin + static MethodChannel channel = const MethodChannel( + 'plugins.flutter.io/firebase_dynamic_links', + ); + + /// The [StreamController] used to update on the latest dynamic link received. + final StreamController _onLinkController = + StreamController.broadcast(); + + /// Gets a [FirebaseDynamicLinksPlatform] with specific arguments such as a different + /// [FirebaseApp]. + @override + FirebaseDynamicLinksPlatform delegateFor({required FirebaseApp app}) { + return MethodChannelFirebaseDynamicLinks(app: app); + } + + /// Attaches generic default values to method channel arguments to allow multi-app support for android. + Map _withChannelDefaults(Map other) { + return { + 'appName': appInstance?.name ?? defaultFirebaseAppName, + }..addAll(other); + } + + PendingDynamicLinkData? _getPendingDynamicLinkDataFromMap( + Map? linkData, + ) { + if (linkData == null) return null; + + final link = linkData['link']; + if (link == null) return null; + + PendingDynamicLinkDataAndroid? androidData; + if (linkData['android'] != null) { + final Map data = linkData['android']; + androidData = PendingDynamicLinkDataAndroid( + clickTimestamp: data['clickTimestamp'], + minimumVersion: data['minimumVersion'], + ); + } + + PendingDynamicLinkDataIOS? iosData; + if (linkData['ios'] != null) { + final Map data = linkData['ios']; + iosData = + PendingDynamicLinkDataIOS(minimumVersion: data['minimumVersion']); + } + + return PendingDynamicLinkData( + link: Uri.parse(link), + android: androidData, + ios: iosData, + ); + } + + @override + Future getInitialLink() async { + try { + final Map? linkData = + await channel.invokeMapMethod( + 'FirebaseDynamicLinks#getInitialLink', + _withChannelDefaults({}), + ); + + return _getPendingDynamicLinkDataFromMap(linkData); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } + + @override + Future getDynamicLink(Uri url) async { + try { + final Map? linkData = + await channel.invokeMapMethod( + 'FirebaseDynamicLinks#getDynamicLink', + _withChannelDefaults({'url': url.toString()}), + ); + + return _getPendingDynamicLinkDataFromMap(linkData); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } + + @override + Stream get onLink { + return _onLinkController.stream; + } + + @override + Future buildLink(DynamicLinkParameters parameters) async { + try { + final String? url = + await MethodChannelFirebaseDynamicLinks.channel.invokeMethod( + 'FirebaseDynamicLinks#buildLink', + _withChannelDefaults(parameters.asMap()), + ); + + return Uri.parse(url!); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } + + @override + Future buildShortLink( + DynamicLinkParameters parameters, { + ShortDynamicLinkType shortLinkType = ShortDynamicLinkType.short, + }) async { + try { + final Map? response = + await MethodChannelFirebaseDynamicLinks.channel + .invokeMapMethod( + 'FirebaseDynamicLinks#buildShortLink', + _withChannelDefaults( + { + 'shortLinkType': shortLinkType.index, + ...parameters.asMap(), + }, + ), + ); + + final List? warnings = response!['warnings']; + return ShortDynamicLink( + type: shortLinkType, + shortUrl: Uri.parse(response['url']), + warnings: warnings?.cast(), + previewLink: response['previewLink'] != null + ? Uri.parse(response['previewLink']) + : null, + ); + } catch (e, s) { + throw convertPlatformException(e, s); + } + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/utils/exception.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/utils/exception.dart new file mode 100644 index 000000000000..a928e64e7864 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/method_channel/utils/exception.dart @@ -0,0 +1,49 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/services.dart'; + +/// Catches a [PlatformException] and returns an [Exception]. +/// +/// If the [Exception] is a [PlatformException], a [FirebaseException] is returned. +Exception convertPlatformException( + Object exception, [ + StackTrace? stackTrace, +]) { + if (exception is! Exception || exception is! PlatformException) { + throw exception; + } + + return platformExceptionToFirebaseException(exception, stackTrace); +} + +/// Converts a [PlatformException] into a [FirebaseException]. +/// +/// A [PlatformException] can only be converted to a [FirebaseException] if the +/// `details` of the exception exist. Firebase returns specific codes and messages +/// which can be converted into user friendly exceptions. +FirebaseException platformExceptionToFirebaseException( + PlatformException platformException, [ + StackTrace? stackTrace, +]) { + Map? details = platformException.details != null + ? Map.from(platformException.details) + : null; + + String code = 'unknown'; + String message = platformException.message ?? ''; + + if (details != null) { + code = details['code'] ?? code; + message = details['message'] ?? message; + } + + return FirebaseException( + plugin: 'firebase_dynamic_links', + code: code, + message: message, + stackTrace: stackTrace, + ); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/navigation_info_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/navigation_info_parameters.dart new file mode 100644 index 000000000000..a9a499d58682 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/navigation_info_parameters.dart @@ -0,0 +1,28 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Options class for defining navigation behavior of the Dynamic Link. +class NavigationInfoParameters { + const NavigationInfoParameters({this.forcedRedirectEnabled}); + + /// Whether forced non-interactive redirect it to be used. + /// + /// Forced non-interactive redirect occurs when link is tapped on mobile + /// device. + /// + /// Default behavior is to disable force redirect and show interstitial page + /// where user tap will initiate navigation to the App (or AppStore if not + /// installed). Disabled force redirect normally improves reliability of the + /// click. + final bool? forcedRedirectEnabled; + + Map asMap() => { + 'forcedRedirectEnabled': forcedRedirectEnabled, + }; + + @override + String toString() { + return '$NavigationInfoParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data.dart new file mode 100644 index 000000000000..512d4bd39871 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data.dart @@ -0,0 +1,38 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'pending_dynamic_link_data_android.dart'; +import 'pending_dynamic_link_data_ios.dart'; + +/// Provides data from received dynamic link. +class PendingDynamicLinkData { + const PendingDynamicLinkData({required this.link, this.android, this.ios}); + + /// Provides Android specific data from received dynamic link. + /// + /// Can be null if [link] equals null or dynamic link was not received on an + /// Android device. + final PendingDynamicLinkDataAndroid? android; + + /// Provides iOS specific data from received dynamic link. + /// + /// Can be null if [link] equals null or dynamic link was not received on an + /// iOS device. + final PendingDynamicLinkDataIOS? ios; + + /// Deep link parameter of the dynamic link. + final Uri link; + + /// Returns the current instance as a [Map]. + Map asMap() => { + 'ios': ios?.asMap(), + 'android': android?.asMap(), + 'link': link.toString(), + }; + + @override + String toString() { + return '$PendingDynamicLinkData($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_android.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_android.dart new file mode 100644 index 000000000000..3fb14e061d35 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_android.dart @@ -0,0 +1,36 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Provides android specific data from received dynamic link. +class PendingDynamicLinkDataAndroid { + const PendingDynamicLinkDataAndroid({ + this.clickTimestamp, + this.minimumVersion, + }); + + /// The time the user clicked on the dynamic link. + /// + /// Equals the number of milliseconds that have elapsed since January 1, 1970. + final int? clickTimestamp; + + /// The minimum version of your app that can open the link. + /// + /// The minimum Android app version requested to process the dynamic link that + /// can be compared directly with versionCode. + /// + /// If the installed app is an older version, the user is taken to the Play + /// Store to upgrade the app. + final int? minimumVersion; + + /// Returns the current instance as a [Map]. + Map asMap() => { + 'clickTimestamp': clickTimestamp, + 'minimumVersion': minimumVersion, + }; + + @override + String toString() { + return '$PendingDynamicLinkDataAndroid($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_ios.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_ios.dart new file mode 100644 index 000000000000..6dde7dcfd23b --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/pending_dynamic_link_data_ios.dart @@ -0,0 +1,24 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Provides iOS specific data from received dynamic link. +class PendingDynamicLinkDataIOS { + const PendingDynamicLinkDataIOS({this.minimumVersion}); + + /// The minimum version of your app that can open the link. + /// + /// It is app developer's responsibility to open AppStore when received link + /// declares higher [minimumVersion] than currently installed. + final String? minimumVersion; + + /// Returns the current instance as a [Map]. + Map asMap() => { + 'minimumVersion': minimumVersion, + }; + + @override + String toString() { + return '$PendingDynamicLinkDataIOS($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/platform_interface/platform_interface_firebase_dynamic_links.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/platform_interface/platform_interface_firebase_dynamic_links.dart new file mode 100644 index 000000000000..8d25d55034f8 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/platform_interface/platform_interface_firebase_dynamic_links.dart @@ -0,0 +1,106 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:meta/meta.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; + +import '../method_channel/method_channel_firebase_dynamic_links.dart'; + +/// Defines an interface to work with Dynamic Links across platforms +abstract class FirebaseDynamicLinksPlatform extends PlatformInterface { + /// The [FirebaseApp] this instance was initialized with. + @protected + final FirebaseApp? appInstance; + + /// Create an instance using [app] + FirebaseDynamicLinksPlatform({this.appInstance}) : super(token: _token); + + /// Returns the [FirebaseApp] for the current instance. + FirebaseApp get app { + return appInstance ?? Firebase.app(); + } + + static final Object _token = Object(); + + /// Create an instance using [app] using the existing implementation + factory FirebaseDynamicLinksPlatform.instanceFor({required FirebaseApp app}) { + return FirebaseDynamicLinksPlatform.instance.delegateFor(app: app); + } + + /// The current default [FirebaseDynamicLinksPlatform] instance. + /// + /// It will always default to [MethodChannelFirebaseDynamicLinks] + /// if no other implementation was provided. + static FirebaseDynamicLinksPlatform get instance { + return _instance ??= MethodChannelFirebaseDynamicLinks(); + } + + static FirebaseDynamicLinksPlatform? _instance; + + /// Sets the [FirebaseFirestorePlatform.instance] + static set instance(FirebaseDynamicLinksPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Enables delegates to create new instances of themselves if a none default + /// [FirebaseApp] instance is required by the user. + @protected + FirebaseDynamicLinksPlatform delegateFor({required FirebaseApp app}) { + throw UnimplementedError('delegateFor() is not implemented'); + } + + /// Creates a stream for listening whenever a dynamic link becomes available + Stream get onLink { + throw UnimplementedError('onLink is not implemented'); + } + + /// Attempts to retrieve the dynamic link which launched the app. + /// + /// This method always returns a Future. That Future completes to null if + /// there is no pending dynamic link or any call to this method after the + /// the first attempt. + Future getInitialLink() { + throw UnimplementedError('getInitialLink() is not implemented'); + } + + /// Determine if the app has a pending dynamic link and provide access to + /// the dynamic link parameters. A pending dynamic link may have been + /// previously captured when a user clicked on a dynamic link, or + /// may be present in the dynamicLinkUri parameter. If both are present, + /// the previously captured dynamic link will take precedence. The captured + /// data will be removed after first access. + Future getDynamicLink(Uri url) async { + throw UnimplementedError('getDynamicLink() is not implemented'); + } + + /// Generate a long Dynamic Link URL. + Future buildLink(DynamicLinkParameters parameters) async { + throw UnimplementedError('buildLink() is not implemented'); + } + + /// Generate a short Dynamic Link URL. + Future buildShortLink( + DynamicLinkParameters parameters, { + ShortDynamicLinkType shortLinkType = ShortDynamicLinkType.short, + }) async { + throw UnimplementedError('buildShortLink() is not implemented'); + } + + @override + //ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) => + other is FirebaseDynamicLinksPlatform && other.app.name == app.name; + + @override + //ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => toString().hashCode; + + @override + String toString() => '$FirebaseDynamicLinksPlatform(app: ${app.name})'; +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link.dart new file mode 100644 index 000000000000..a988846f3839 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link.dart @@ -0,0 +1,39 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'short_dynamic_link_type.dart'; + +/// Response from creating a short dynamic link with [DynamicLinkBuilder]. +class ShortDynamicLink { + const ShortDynamicLink({ + required this.type, + required this.shortUrl, + this.warnings, + this.previewLink, + }); + + /// Short url value. + final Uri shortUrl; + + /// The type of this short link, e.g. [ShortDynamicLinkType.unguessable]. + final ShortDynamicLinkType type; + + /// Gets the preview link to show the link flow chart. Android only. + final Uri? previewLink; + + /// Information about potential warnings on link creation. + final List? warnings; + + /// Returns the current instance as a [Map]. + Map asMap() => { + 'shortUrl': shortUrl.toString(), + 'previewLink': previewLink.toString(), + 'warnings': warnings, + }; + + @override + String toString() { + return '$ShortDynamicLink($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link_type.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link_type.dart new file mode 100644 index 000000000000..c758803c2732 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/short_dynamic_link_type.dart @@ -0,0 +1,16 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Enum used to define the desired path length for shortened Dynamic Link URLs. +enum ShortDynamicLinkType { + /// Shorten the path to an unguessable string. Such strings are created by base62-encoding randomly + /// generated 96-bit numbers, and consist of 17 alphanumeric characters. Use unguessable strings + /// to prevent your Dynamic DynamicLinks from being crawled, which can potentially expose sensitive information. + unguessable, + + /// Shorten the path to a string that is only as long as needed to be unique, with a minimum length + /// of 4 characters. Use this if sensitive information would not be exposed if a short + /// Dynamic Link URL were guessed. + short, +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/social_meta_tag_parameters.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/social_meta_tag_parameters.dart new file mode 100644 index 000000000000..46a8fbca9890 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/lib/src/social_meta_tag_parameters.dart @@ -0,0 +1,28 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The Dynamic Link Social Meta Tag parameters. +class SocialMetaTagParameters { + const SocialMetaTagParameters({this.description, this.imageUrl, this.title}); + + /// The description to use when the Dynamic Link is shared in a social post. + final String? description; + + /// The URL to an image related to this link. + final Uri? imageUrl; + + /// The title to use when the Dynamic Link is shared in a social post. + final String? title; + + Map asMap() => { + 'description': description, + 'imageUrl': imageUrl?.toString(), + 'title': title, + }; + + @override + String toString() { + return '$SocialMetaTagParameters($asMap)'; + } +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/pubspec.yaml b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..63753bad63a5 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +name: firebase_dynamic_links_platform_interface +description: A common platform interface for the firebase_dynamic_links plugin. +version: 0.1.0 +homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface +repository: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.5" + +dependencies: + firebase_core: ^1.8.0 + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + + +dev_dependencies: + firebase_core_platform_interface: ^4.0.1 + flutter_test: + sdk: flutter + mockito: ^5.0.0 diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/android_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/android_parameters_test.dart new file mode 100644 index 000000000000..fcbcf6a3325e --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/android_parameters_test.dart @@ -0,0 +1,47 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Uri fallbackUrl = Uri.parse('fallbackUrl'); + const String packageName = 'packageName'; + const int minimumVersion = 21; + + group('$AndroidParameters', () { + AndroidParameters androidParams = AndroidParameters( + fallbackUrl: fallbackUrl, + minimumVersion: minimumVersion, + packageName: packageName, + ); + group('Constructor', () { + test('returns an instance of [AndroidParameters]', () { + expect(androidParams, isA()); + expect(androidParams.fallbackUrl, fallbackUrl); + expect(androidParams.minimumVersion, minimumVersion); + expect(androidParams.packageName, packageName); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = androidParams.asMap(); + + expect(result, isA>()); + + expect(result['fallbackUrl'], fallbackUrl.toString()); + expect(result['minimumVersion'], minimumVersion); + expect(result['packageName'], packageName); + }); + }); + + test('toString', () { + expect( + androidParams.toString(), + equals('$AndroidParameters(${androidParams.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/dynamic_link_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/dynamic_link_parameters_test.dart new file mode 100644 index 000000000000..f0a02ff84217 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/dynamic_link_parameters_test.dart @@ -0,0 +1,184 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + AndroidParameters androidParams = AndroidParameters( + fallbackUrl: Uri.parse('test-url'), + minimumVersion: 1, + packageName: 'test-package', + ); + + GoogleAnalyticsParameters googleParams = const GoogleAnalyticsParameters( + campaign: 'campaign', + medium: 'medium', + source: 'source', + term: 'term', + content: 'content', + ); + + IOSParameters iosParams = IOSParameters( + appStoreId: 'appStoreId', + bundleId: 'bundleId', + customScheme: 'customScheme', + fallbackUrl: Uri.parse('fallbackUrl'), + ipadBundleId: 'ipadBundleId', + ipadFallbackUrl: Uri.parse('ipadFallbackUrl'), + minimumVersion: 'minimumVersion', + ); + + ITunesConnectAnalyticsParameters itunesParams = + const ITunesConnectAnalyticsParameters( + affiliateToken: 'affiliateToken', + campaignToken: 'campaignToken', + providerToken: 'providerToken', + ); + + Uri link = Uri.parse('link'); + NavigationInfoParameters navigation = + const NavigationInfoParameters(forcedRedirectEnabled: true); + SocialMetaTagParameters social = SocialMetaTagParameters( + description: 'description', + imageUrl: Uri.parse('imageUrl'), + title: 'title', + ); + + String uriPrefix = 'https://'; + + group('$DynamicLinkParameters', () { + DynamicLinkParameters dynamicLinkParams = DynamicLinkParameters( + uriPrefix: uriPrefix, + link: link, + androidParameters: androidParams, + googleAnalyticsParameters: googleParams, + iosParameters: iosParams, + itunesConnectAnalyticsParameters: itunesParams, + navigationInfoParameters: navigation, + socialMetaTagParameters: social, + ); + group('Constructor', () { + test('returns an instance of [DynamicLinkParameters]', () { + expect(dynamicLinkParams, isA()); + expect(dynamicLinkParams.androidParameters, androidParams); + expect(dynamicLinkParams.uriPrefix, uriPrefix); + expect(dynamicLinkParams.link, link); + expect(dynamicLinkParams.googleAnalyticsParameters, googleParams); + expect(dynamicLinkParams.iosParameters, iosParams); + expect( + dynamicLinkParams.itunesConnectAnalyticsParameters, + itunesParams, + ); + expect(dynamicLinkParams.navigationInfoParameters, navigation); + expect(dynamicLinkParams.socialMetaTagParameters, social); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = dynamicLinkParams.asMap(); + + expect(result, isA>()); + expect( + result['androidParameters']['fallbackUrl'], + dynamicLinkParams.androidParameters?.fallbackUrl.toString(), + ); + expect( + result['androidParameters']['minimumVersion'], + dynamicLinkParams.androidParameters?.minimumVersion, + ); + expect( + result['androidParameters']['packageName'], + dynamicLinkParams.androidParameters?.packageName, + ); + expect(result['uriPrefix'], dynamicLinkParams.uriPrefix); + expect( + result['googleAnalyticsParameters']['campaign'], + dynamicLinkParams.googleAnalyticsParameters?.campaign, + ); + expect( + result['googleAnalyticsParameters']['content'], + dynamicLinkParams.googleAnalyticsParameters?.content, + ); + expect( + result['googleAnalyticsParameters']['medium'], + dynamicLinkParams.googleAnalyticsParameters?.medium, + ); + expect( + result['googleAnalyticsParameters']['source'], + dynamicLinkParams.googleAnalyticsParameters?.source, + ); + expect( + result['googleAnalyticsParameters']['term'], + dynamicLinkParams.googleAnalyticsParameters?.term, + ); + expect( + result['iosParameters']['appStoreId'], + dynamicLinkParams.iosParameters?.appStoreId, + ); + expect( + result['iosParameters']['bundleId'], + dynamicLinkParams.iosParameters?.bundleId, + ); + expect( + result['iosParameters']['customScheme'], + dynamicLinkParams.iosParameters?.customScheme, + ); + expect( + result['iosParameters']['fallbackUrl'], + dynamicLinkParams.iosParameters?.fallbackUrl.toString(), + ); + expect( + result['iosParameters']['ipadBundleId'], + dynamicLinkParams.iosParameters?.ipadBundleId, + ); + expect( + result['iosParameters']['ipadFallbackUrl'], + dynamicLinkParams.iosParameters?.ipadFallbackUrl.toString(), + ); + expect( + result['iosParameters']['minimumVersion'], + dynamicLinkParams.iosParameters?.minimumVersion, + ); + expect( + result['itunesConnectAnalyticsParameters']['affiliateToken'], + dynamicLinkParams.itunesConnectAnalyticsParameters?.affiliateToken, + ); + expect( + result['itunesConnectAnalyticsParameters']['providerToken'], + dynamicLinkParams.itunesConnectAnalyticsParameters?.providerToken, + ); + expect( + result['itunesConnectAnalyticsParameters']['campaignToken'], + dynamicLinkParams.itunesConnectAnalyticsParameters?.campaignToken, + ); + expect(result['link'], dynamicLinkParams.link.toString()); + expect( + result['navigationInfoParameters']['forcedRedirectEnabled'], + dynamicLinkParams.navigationInfoParameters?.forcedRedirectEnabled, + ); + expect( + result['socialMetaTagParameters']['description'], + dynamicLinkParams.socialMetaTagParameters?.description, + ); + expect( + result['socialMetaTagParameters']['imageUrl'], + dynamicLinkParams.socialMetaTagParameters?.imageUrl.toString(), + ); + expect( + result['socialMetaTagParameters']['title'], + dynamicLinkParams.socialMetaTagParameters?.title, + ); + }); + }); + + test('toString', () { + expect( + dynamicLinkParams.toString(), + equals('$DynamicLinkParameters(${dynamicLinkParams.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/ios_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/ios_parameters_test.dart new file mode 100644 index 000000000000..b1e9fd4f9dcc --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/ios_parameters_test.dart @@ -0,0 +1,67 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + String appStoreId = 'appStoreId'; + String bundleId = 'bundleId'; + String customScheme = 'customScheme'; + String ipadBundleId = 'ipadBundleId'; + String minimumVersion = 'minimumVersion'; + Uri fallbackUrl = Uri.parse('fallbackUrl'); + Uri ipadFallbackUrl = Uri.parse('ipadFallbackUrl'); + + group('$IOSParameters', () { + IOSParameters iosParams = IOSParameters( + appStoreId: appStoreId, + bundleId: bundleId, + customScheme: customScheme, + fallbackUrl: fallbackUrl, + ipadBundleId: ipadBundleId, + ipadFallbackUrl: ipadFallbackUrl, + minimumVersion: minimumVersion, + ); + + group('Constructor', () { + test('returns an instance of [IosParameters]', () { + expect(iosParams, isA()); + expect(iosParams.appStoreId, appStoreId); + expect(iosParams.bundleId, bundleId); + expect(iosParams.bundleId, bundleId); + expect(iosParams.customScheme, customScheme); + expect(iosParams.fallbackUrl, fallbackUrl); + expect(iosParams.ipadBundleId, ipadBundleId); + expect(iosParams.ipadFallbackUrl, ipadFallbackUrl); + expect(iosParams.minimumVersion, minimumVersion); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = iosParams.asMap(); + + expect(result, isA>()); + expect(result['appStoreId'], iosParams.appStoreId); + expect(result['bundleId'], iosParams.bundleId); + expect(result['customScheme'], iosParams.customScheme); + expect(result['fallbackUrl'], iosParams.fallbackUrl.toString()); + expect( + result['ipadFallbackUrl'], + iosParams.ipadFallbackUrl.toString(), + ); + expect(result['ipadBundleId'], iosParams.ipadBundleId); + expect(result['minimumVersion'], iosParams.minimumVersion); + }); + }); + + test('toString', () { + expect( + iosParams.toString(), + equals('$IOSParameters(${iosParams.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/itunes_connect_analytics_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/itunes_connect_analytics_parameters_test.dart new file mode 100644 index 000000000000..fa208cdd6d5b --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/itunes_connect_analytics_parameters_test.dart @@ -0,0 +1,48 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + String affiliateToken = 'affiliateToken'; + String campaignToken = 'campaignToken'; + String providerToken = 'providerToken'; + + group('$ITunesConnectAnalyticsParameters', () { + ITunesConnectAnalyticsParameters itunesParams = + ITunesConnectAnalyticsParameters( + affiliateToken: affiliateToken, + campaignToken: campaignToken, + providerToken: providerToken, + ); + + group('Constructor', () { + test('returns an instance of [ItunesConnectAnalyticsParameters]', () { + expect(itunesParams, isA()); + expect(itunesParams.affiliateToken, affiliateToken); + expect(itunesParams.campaignToken, campaignToken); + expect(itunesParams.providerToken, providerToken); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = itunesParams.asMap(); + + expect(result, isA>()); + expect(result['affiliateToken'], itunesParams.affiliateToken); + expect(result['campaignToken'], itunesParams.campaignToken); + expect(result['providerToken'], itunesParams.providerToken); + }); + }); + + test('toString', () { + expect( + itunesParams.toString(), + equals('$ITunesConnectAnalyticsParameters(${itunesParams.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/method_channel_tests/method_channel_firebase_dynamic_links_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/method_channel_tests/method_channel_firebase_dynamic_links_test.dart new file mode 100644 index 000000000000..eca1d7cf5d90 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/method_channel_tests/method_channel_firebase_dynamic_links_test.dart @@ -0,0 +1,388 @@ +// ignore_for_file: require_trailing_commas +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:firebase_dynamic_links_platform_interface/src/method_channel/method_channel_firebase_dynamic_links.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../mock.dart'; + +DynamicLinkParameters buildDynamicLinkParameters() { + AndroidParameters android = AndroidParameters( + fallbackUrl: Uri.parse('fallbackUrl'), + minimumVersion: 1, + packageName: 'test-package', + ); + + GoogleAnalyticsParameters google = const GoogleAnalyticsParameters( + campaign: 'campaign', + medium: 'medium', + source: 'source', + term: 'term', + content: 'content', + ); + + IOSParameters ios = IOSParameters( + appStoreId: 'appStoreId', + bundleId: 'bundleId', + customScheme: 'customScheme', + fallbackUrl: Uri.parse('fallbackUrl'), + ipadBundleId: 'ipadBundleId', + ipadFallbackUrl: Uri.parse('ipadFallbackUrl'), + minimumVersion: 'minimumVersion'); + + ITunesConnectAnalyticsParameters itunes = + const ITunesConnectAnalyticsParameters( + affiliateToken: 'affiliateToken', + campaignToken: 'campaignToken', + providerToken: 'providerToken', + ); + + Uri link = Uri.parse('link'); + NavigationInfoParameters navigation = + const NavigationInfoParameters(forcedRedirectEnabled: true); + SocialMetaTagParameters social = SocialMetaTagParameters( + description: 'description', + imageUrl: Uri.parse('imageUrl'), + title: 'title'); + + String uriPrefix = 'https://'; + + return DynamicLinkParameters( + uriPrefix: uriPrefix, + link: link, + androidParameters: android, + googleAnalyticsParameters: google, + iosParameters: ios, + itunesConnectAnalyticsParameters: itunes, + navigationInfoParameters: navigation, + socialMetaTagParameters: social, + ); +} + +void main() { + setupFirebaseDynamicLinksMocks(); + + bool mockPlatformExceptionThrown = false; + + late FirebaseDynamicLinksPlatform dynamicLinks; + final List logger = []; + int getInitialLinkCall = 1; + + group('$MethodChannelFirebaseDynamicLinks', () { + setUpAll(() async { + FirebaseApp app = await Firebase.initializeApp(); + + handleMethodCall((call) async { + logger.add(call); + + if (mockPlatformExceptionThrown) { + throw PlatformException(code: 'UNKNOWN'); + } + + final Map returnUrl = { + 'url': 'google.com', + 'warnings': ['This is only a test link'], + }; + switch (call.method) { + case 'FirebaseDynamicLinks#buildLink': + return 'google.com'; + case 'FirebaseDynamicLinks#buildShortLink': + return returnUrl; + case 'FirebaseDynamicLink#onLinkSuccess': + const String name = 'FirebaseDynamicLink#onLinkSuccess'; + handleEventChannel(name, logger); + return name; + case 'FirebaseDynamicLinks#getInitialLink': + if (getInitialLinkCall == 3) { + return null; + } + return { + 'link': getInitialLinkCall == 2 ? null : 'https://google.com', + 'android': { + 'clickTimestamp': 1234567, + 'minimumVersion': 12, + }, + 'ios': { + 'minimumVersion': 'Version 12', + }, + }; + case 'FirebaseDynamicLinks#getDynamicLink': + return { + 'link': 'https://google.com', + }; + default: + return null; + } + }); + + dynamicLinks = MethodChannelFirebaseDynamicLinks(app: app); + }); + + setUp(() async { + logger.clear(); + }); + + tearDown(() async { + mockPlatformExceptionThrown = false; + }); + + group('getInitialLink()', () { + test('link can be parsed', () async { + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + + expect(data!.link, Uri.parse('https://google.com')); + + expect(data.android!.clickTimestamp, 1234567); + expect(data.android!.minimumVersion, 12); + + expect(data.ios!.minimumVersion, 'Version 12'); + + expect(logger, [ + isMethodCall( + 'FirebaseDynamicLinks#getInitialLink', + arguments: { + 'appName': '[DEFAULT]', + }, + ) + ]); + }); + + // Both iOS FIRDynamicLink.url and android PendingDynamicLinkData.getUrl() + // might return null link. In such a case we want to ignore the deep-link. + test('for null link, return null', () async { + getInitialLinkCall = 2; + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + + expect(data, isNull); + + expect(logger, [ + isMethodCall( + 'FirebaseDynamicLinks#getInitialLink', + arguments: { + 'appName': '[DEFAULT]', + }, + ) + ]); + }); + + test('for null result, returns null', () async { + getInitialLinkCall = 3; + + final PendingDynamicLinkData? data = + await dynamicLinks.getInitialLink(); + + expect(data, isNull); + + expect(logger, [ + isMethodCall( + 'FirebaseDynamicLinks#getInitialLink', + arguments: { + 'appName': '[DEFAULT]', + }, + ) + ]); + }); + + test( + 'catch a [PlatformException] error and throws a [FirebaseException] error', + () async { + mockPlatformExceptionThrown = true; + + await testExceptionHandling(dynamicLinks.getInitialLink); + }); + }); + + group('getDynamicLink()', () { + test('getDynamicLink', () async { + final Uri argument = Uri.parse('short-link'); + final PendingDynamicLinkData? data = + await dynamicLinks.getDynamicLink(argument); + + expect(data!.link.host, 'google.com'); + + expect(logger, [ + isMethodCall('FirebaseDynamicLinks#getDynamicLink', + arguments: { + 'url': argument.toString(), + 'appName': '[DEFAULT]', + }) + ]); + }); + + test( + 'catch a [PlatformException] error and throws a [FirebaseException] error', + () async { + mockPlatformExceptionThrown = true; + final Uri argument = Uri.parse('short-link'); + await testExceptionHandling( + () => dynamicLinks.getDynamicLink(argument)); + }); + }); + + group('buildLink()', () { + test('buildLink', () async { + DynamicLinkParameters options = buildDynamicLinkParameters(); + + await dynamicLinks.buildLink(options); + + expect(logger, [ + isMethodCall( + 'FirebaseDynamicLinks#buildLink', + arguments: { + 'appName': '[DEFAULT]', + 'uriPrefix': 'https://', + 'link': 'link', + 'androidParameters': { + 'fallbackUrl': 'fallbackUrl', + 'minimumVersion': 1, + 'packageName': 'test-package' + }, + 'googleAnalyticsParameters': { + 'campaign': 'campaign', + 'content': 'content', + 'medium': 'medium', + 'source': 'source', + 'term': 'term' + }, + 'iosParameters': { + 'appStoreId': 'appStoreId', + 'bundleId': 'bundleId', + 'customScheme': 'customScheme', + 'fallbackUrl': 'fallbackUrl', + 'ipadBundleId': 'ipadBundleId', + 'ipadFallbackUrl': 'ipadFallbackUrl', + 'minimumVersion': 'minimumVersion', + }, + 'itunesConnectAnalyticsParameters': { + 'affiliateToken': 'affiliateToken', + 'campaignToken': 'campaignToken', + 'providerToken': 'providerToken', + }, + 'navigationInfoParameters': { + 'forcedRedirectEnabled': true, + }, + 'socialMetaTagParameters': { + 'description': 'description', + 'imageUrl': 'imageUrl', + 'title': 'title', + }, + }, + ), + ]); + }); + + test( + 'catch a [PlatformException] error and throws a [FirebaseException] error', + () async { + mockPlatformExceptionThrown = true; + DynamicLinkParameters options = buildDynamicLinkParameters(); + + await testExceptionHandling(() => dynamicLinks.buildLink(options)); + }); + }); + + group('onLink()', () { + StreamSubscription? subscription; + + tearDown(() { + subscription?.cancel(); + }); + + test('returns [Stream]', () async { + // Checks that `onLink` does not throw UnimplementedError + expect(dynamicLinks.onLink, isNotNull); + }); + + test('listens to incoming changes', () async { + // Stream stream = + // dynamicLinks.onLink().asBroadcastStream(); + // + // await injectEventChannelResponse('FirebaseDynamicLink#onLinkSuccess', { + // 'link': 'link', + // 'ios': {'minimumVersion': 'minimumVersion'} + // }); + // TODO find out why event isn't emitted. also catch error. + // await expectLater( + // stream, + // emits(isA() + // .having((r) => r.link, 'link', 'link')), + // ); + }); + }); + group('buildShortLink()', () { + test('buildShortLink', () async { + DynamicLinkParameters options = buildDynamicLinkParameters(); + + await dynamicLinks.buildShortLink(options); + + expect(logger, [ + isMethodCall( + 'FirebaseDynamicLinks#buildShortLink', + arguments: { + 'appName': '[DEFAULT]', + 'shortLinkType': ShortDynamicLinkType.short.index, + 'uriPrefix': 'https://', + 'link': 'link', + 'androidParameters': { + 'fallbackUrl': 'fallbackUrl', + 'minimumVersion': 1, + 'packageName': 'test-package' + }, + 'googleAnalyticsParameters': { + 'campaign': 'campaign', + 'content': 'content', + 'medium': 'medium', + 'source': 'source', + 'term': 'term' + }, + 'iosParameters': { + 'appStoreId': 'appStoreId', + 'bundleId': 'bundleId', + 'customScheme': 'customScheme', + 'fallbackUrl': 'fallbackUrl', + 'ipadBundleId': 'ipadBundleId', + 'ipadFallbackUrl': 'ipadFallbackUrl', + 'minimumVersion': 'minimumVersion', + }, + 'itunesConnectAnalyticsParameters': { + 'affiliateToken': 'affiliateToken', + 'campaignToken': 'campaignToken', + 'providerToken': 'providerToken', + }, + 'navigationInfoParameters': { + 'forcedRedirectEnabled': true, + }, + 'socialMetaTagParameters': { + 'description': 'description', + 'imageUrl': 'imageUrl', + 'title': 'title', + }, + }, + ), + ]); + }); + + test( + 'catch a [PlatformException] error and throws a [FirebaseException] error', + () async { + mockPlatformExceptionThrown = true; + DynamicLinkParameters options = buildDynamicLinkParameters(); + + await testExceptionHandling(() => dynamicLinks.buildShortLink(options)); + }); + }); + }); +} + +class TestMethodChannelFirebaseDynamicLinks + extends MethodChannelFirebaseDynamicLinks { + TestMethodChannelFirebaseDynamicLinks(FirebaseApp app) : super(app: app); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/mock.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/mock.dart new file mode 100644 index 000000000000..b82bea110a87 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/mock.dart @@ -0,0 +1,93 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:firebase_dynamic_links_platform_interface/src/method_channel/method_channel_firebase_dynamic_links.dart'; + +typedef MethodCallCallback = dynamic Function(MethodCall methodCall); +typedef Callback = void Function(MethodCall call); + +int mockHandleId = 0; + +int get nextMockHandleId => mockHandleId++; + +void setupFirebaseDynamicLinksMocks([Callback? customHandlers]) { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelFirebase.channel.setMockMethodCallHandler((call) async { + if (call.method == 'Firebase#initializeCore') { + return [ + { + 'name': defaultFirebaseAppName, + 'options': { + 'apiKey': '123', + 'appId': '123', + 'messagingSenderId': '123', + 'projectId': '123', + }, + 'pluginConstants': {}, + } + ]; + } + + if (call.method == 'Firebase#initializeApp') { + return { + 'name': call.arguments['appName'], + 'options': call.arguments['options'], + 'pluginConstants': {}, + }; + } + + if (customHandlers != null) { + customHandlers(call); + } + + return null; + }); +} + +void handleMethodCall(MethodCallCallback methodCallCallback) => + MethodChannelFirebaseDynamicLinks.channel + .setMockMethodCallHandler((call) async { + return await methodCallCallback(call); + }); + +void handleEventChannel( + final String name, [ + List? log, +]) { + MethodChannel(name).setMockMethodCallHandler((MethodCall methodCall) async { + log?.add(methodCall); + switch (methodCall.method) { + case 'listen': + break; + case 'cancel': + default: + return null; + } + }); +} + +Future injectEventChannelResponse( + String channelName, + Map event, +) async { + await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + channelName, + MethodChannelFirebaseDynamicLinks.channel.codec + .encodeSuccessEnvelope(event), + (_) {}, + ); +} + +Future testExceptionHandling( + void Function() testMethod, +) async { + await expectLater( + () async => testMethod(), + anyOf([completes, throwsA(isA())]), + ); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/navigation_info_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/navigation_info_parameters_test.dart new file mode 100644 index 000000000000..6eef03b26a35 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/navigation_info_parameters_test.dart @@ -0,0 +1,41 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + bool forcedRedirectEnabled = true; + + group('$NavigationInfoParameters', () { + NavigationInfoParameters navParams = + NavigationInfoParameters(forcedRedirectEnabled: forcedRedirectEnabled); + + group('Constructor', () { + test('returns an instance of [NavigationInfoParameters]', () { + expect(navParams, isA()); + expect(navParams.forcedRedirectEnabled, forcedRedirectEnabled); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = navParams.asMap(); + + expect(result, isA>()); + expect( + result['forcedRedirectEnabled'], + navParams.forcedRedirectEnabled, + ); + }); + }); + + test('toString', () { + expect( + navParams.toString(), + equals('$NavigationInfoParameters(${navParams.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/pending_dynamic_link_data_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/pending_dynamic_link_data_test.dart new file mode 100644 index 000000000000..84d7ec326ee0 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/pending_dynamic_link_data_test.dart @@ -0,0 +1,71 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Uri link = Uri.parse('pending-link'); + int minimumVersion = 12; + String minimumVersionIos = 'minimum version'; + int clickTimestamp = 12345345; + PendingDynamicLinkDataAndroid androidData = PendingDynamicLinkDataAndroid( + minimumVersion: minimumVersion, + clickTimestamp: clickTimestamp, + ); + PendingDynamicLinkDataIOS iosData = + PendingDynamicLinkDataIOS(minimumVersion: minimumVersionIos); + + group('$PendingDynamicLinkData', () { + PendingDynamicLinkData pendingDynamicLinkData = + PendingDynamicLinkData(link: link, android: androidData, ios: iosData); + + group('Constructor', () { + test('returns an instance of [PendingDynamicLinkData]', () { + expect(pendingDynamicLinkData, isA()); + expect(pendingDynamicLinkData.link, link); + expect( + pendingDynamicLinkData.android?.clickTimestamp, + androidData.clickTimestamp, + ); + expect( + pendingDynamicLinkData.android?.minimumVersion, + androidData.minimumVersion, + ); + expect( + pendingDynamicLinkData.ios?.minimumVersion, + iosData.minimumVersion, + ); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = pendingDynamicLinkData.asMap(); + + expect(result, isA>()); + expect( + result['android']['clickTimestamp'], + pendingDynamicLinkData.android?.clickTimestamp, + ); + expect( + result['android']['minimumVersion'], + pendingDynamicLinkData.android?.minimumVersion, + ); + expect( + result['ios']['minimumVersion'], + pendingDynamicLinkData.ios?.minimumVersion, + ); + expect(result['link'], pendingDynamicLinkData.link.toString()); + }); + }); + + test('toString', () { + expect( + pendingDynamicLinkData.toString(), + equals('$PendingDynamicLinkData(${pendingDynamicLinkData.asMap})'), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/platform_interface_tests/platform_interface_firebase_dynamic_links_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/platform_interface_tests/platform_interface_firebase_dynamic_links_test.dart new file mode 100644 index 000000000000..e56cedcd2e4a --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/platform_interface_tests/platform_interface_firebase_dynamic_links_test.dart @@ -0,0 +1,144 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../mock.dart'; + +void main() { + setupFirebaseDynamicLinksMocks(); + + TestFirebaseDynamicLinksPlatform? firebaseDynamicLinksPlatformPlatform; + + FirebaseApp? app; + FirebaseApp? secondaryApp; + final link = Uri.parse('uri'); + final parameters = DynamicLinkParameters(uriPrefix: '', link: link); + + group('$FirebaseDynamicLinksPlatform()', () { + setUpAll(() async { + app = await Firebase.initializeApp(); + secondaryApp = await Firebase.initializeApp( + name: 'testApp2', + options: const FirebaseOptions( + appId: '1:1234567890:ios:42424242424242', + apiKey: '123', + projectId: '123', + messagingSenderId: '1234567890', + ), + ); + + firebaseDynamicLinksPlatformPlatform = TestFirebaseDynamicLinksPlatform( + app!, + ); + }); + + test('Constructor', () { + expect( + firebaseDynamicLinksPlatformPlatform, + isA(), + ); + expect(firebaseDynamicLinksPlatformPlatform, isA()); + }); + + test('get.instance', () { + expect( + FirebaseDynamicLinksPlatform.instance, + isA(), + ); + expect( + FirebaseDynamicLinksPlatform.instance.app.name, + equals(defaultFirebaseAppName), + ); + }); + + group('set.instance', () { + test('sets the current instance', () { + FirebaseDynamicLinksPlatform.instance = + TestFirebaseDynamicLinksPlatform(secondaryApp!); + + expect( + FirebaseDynamicLinksPlatform.instance, + isA(), + ); + expect( + FirebaseDynamicLinksPlatform.instance.app.name, + equals('testApp2'), + ); + }); + }); + + test('throws if .getInitialLink', () { + expect( + () => firebaseDynamicLinksPlatformPlatform!.getInitialLink(), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'getInitialLink() is not implemented', + ), + ), + ); + }); + + test('throws if .getDynamicLink', () { + expect( + () => firebaseDynamicLinksPlatformPlatform!.getDynamicLink(link), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'getDynamicLink() is not implemented', + ), + ), + ); + }); + + test('throws if .onLink', () { + expect( + () => firebaseDynamicLinksPlatformPlatform!.onLink, + throwsA( + isA().having( + (e) => e.message, + 'message', + 'onLink is not implemented', + ), + ), + ); + }); + + test('throws if .buildLink', () { + expect( + () => firebaseDynamicLinksPlatformPlatform!.buildLink(parameters), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'buildLink() is not implemented', + ), + ), + ); + }); + + test('throws if .buildShortLink', () { + expect( + () => firebaseDynamicLinksPlatformPlatform!.buildShortLink(parameters), + throwsA( + isA().having( + (e) => e.message, + 'message', + 'buildShortLink() is not implemented', + ), + ), + ); + }); + }); +} + +class TestFirebaseDynamicLinksPlatform extends FirebaseDynamicLinksPlatform { + TestFirebaseDynamicLinksPlatform(FirebaseApp app) : super(appInstance: app); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/short_dynamic_link_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/short_dynamic_link_test.dart new file mode 100644 index 000000000000..18758a376f3e --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/short_dynamic_link_test.dart @@ -0,0 +1,51 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + Uri link = Uri.parse('short-link'); + Uri previewLink = Uri.parse('preview-link'); + List warnings = ['warning']; + + group('$ShortDynamicLink', () { + ShortDynamicLink shortLink = ShortDynamicLink( + type: ShortDynamicLinkType.short, + shortUrl: link, + previewLink: previewLink, + warnings: warnings, + ); + + group('Constructor', () { + test('returns an instance of [ShortDynamicLink]', () { + expect(shortLink, isA()); + expect(shortLink.shortUrl, link); + expect(shortLink.type, ShortDynamicLinkType.short); + expect(shortLink.previewLink, previewLink); + expect(shortLink.warnings, warnings); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = shortLink.asMap(); + + expect(result, isA>()); + expect(result['shortUrl'], shortLink.shortUrl.toString()); + expect(result['previewLink'], shortLink.previewLink.toString()); + expect(result['warnings'], shortLink.warnings); + }); + }); + + test('toString', () { + expect( + shortLink.toString(), + equals( + '$ShortDynamicLink(${shortLink.asMap})', + ), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/social_meta_tag_parameters_test.dart b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/social_meta_tag_parameters_test.dart new file mode 100644 index 000000000000..61bf0a0a2153 --- /dev/null +++ b/packages/firebase_dynamic_links/firebase_dynamic_links_platform_interface/test/social_meta_tag_parameters_test.dart @@ -0,0 +1,52 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_dynamic_links_platform_interface/firebase_dynamic_links_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + String description = 'description'; + String title = 'title'; + Uri imageUrl = Uri.parse('imageUrl'); + + group('$SocialMetaTagParameters', () { + SocialMetaTagParameters socialMetaTagParameters = SocialMetaTagParameters( + description: description, + title: title, + imageUrl: imageUrl, + ); + + group('Constructor', () { + test('returns an instance of [SocialMetaTagParameters]', () { + expect(socialMetaTagParameters, isA()); + expect(socialMetaTagParameters.description, description); + expect(socialMetaTagParameters.title, title); + expect(socialMetaTagParameters.imageUrl, imageUrl); + }); + + group('asMap', () { + test('returns the current instance as a [Map]', () { + final result = socialMetaTagParameters.asMap(); + + expect(result, isA>()); + expect(result['description'], socialMetaTagParameters.description); + expect(result['title'], socialMetaTagParameters.title); + expect( + result['imageUrl'], + socialMetaTagParameters.imageUrl.toString(), + ); + }); + }); + + test('toString', () { + expect( + socialMetaTagParameters.toString(), + equals( + '$SocialMetaTagParameters(${socialMetaTagParameters.asMap})', + ), + ); + }); + }); + }); +} diff --git a/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h b/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h deleted file mode 100644 index f905bbbd03e0..000000000000 --- a/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTFirebaseDynamicLinksPlugin : NSObject -@end diff --git a/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m b/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m deleted file mode 100644 index 0831fe6b0a23..000000000000 --- a/packages/firebase_dynamic_links/ios/Classes/FLTFirebaseDynamicLinksPlugin.m +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTFirebaseDynamicLinksPlugin.h" - -#import - -static FlutterError *getFlutterError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] - message:error.domain - details:error.localizedDescription]; -} - -static NSMutableDictionary *getDictionaryFromDynamicLink(FIRDynamicLink *dynamicLink) { - if (dynamicLink != nil) { - NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; - dictionary[@"link"] = dynamicLink.url.absoluteString; - - NSMutableDictionary *iosData = [[NSMutableDictionary alloc] init]; - if (dynamicLink.minimumAppVersion) { - iosData[@"minimumVersion"] = dynamicLink.minimumAppVersion; - } - dictionary[@"ios"] = iosData; - return dictionary; - } else { - return nil; - } -} - -static NSMutableDictionary *getDictionaryFromFlutterError(FlutterError *error) { - if (error == nil) { - return nil; - } - - NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; - dictionary[@"code"] = error.code; - dictionary[@"message"] = error.message; - dictionary[@"details"] = error.details; - return dictionary; -} - -@interface FLTFirebaseDynamicLinksPlugin () -@property(nonatomic, retain) FlutterMethodChannel *channel; -@property(nonatomic, retain) FIRDynamicLink *initialLink; -@property(nonatomic, retain) FlutterError *flutterError; -@property(nonatomic) BOOL initiated; -@end - -@implementation FLTFirebaseDynamicLinksPlugin -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_dynamic_links" - binaryMessenger:[registrar messenger]]; - FLTFirebaseDynamicLinksPlugin *instance = - [[FLTFirebaseDynamicLinksPlugin alloc] initWithChannel:channel]; - [registrar addMethodCallDelegate:instance channel:channel]; - [registrar addApplicationDelegate:instance]; - - SEL sel = NSSelectorFromString(@"registerLibrary:withVersion:"); - if ([FIRApp respondsToSelector:sel]) { - [FIRApp performSelector:sel withObject:LIBRARY_NAME withObject:LIBRARY_VERSION]; - } -} - -- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { - self = [super init]; - if (self) { - _initiated = NO; - _channel = channel; - if (![FIRApp appNamed:@"__FIRAPP_DEFAULT"]) { - NSLog(@"Configuring the default Firebase app..."); - [FIRApp configure]; - NSLog(@"Configured the default Firebase app %@.", [FIRApp defaultApp].name); - } - } - return self; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"DynamicLinkParameters#buildUrl" isEqualToString:call.method]) { - FIRDynamicLinkComponents *components = [self setupParameters:call.arguments]; - result([components.url absoluteString]); - } else if ([@"DynamicLinkParameters#buildShortLink" isEqualToString:call.method]) { - FIRDynamicLinkComponents *components = [self setupParameters:call.arguments]; - [components shortenWithCompletion:[self createShortLinkCompletion:result]]; - } else if ([@"DynamicLinkParameters#shortenUrl" isEqualToString:call.method]) { - FIRDynamicLinkComponentsOptions *options = [self setupOptions:call.arguments]; - NSURL *url = [NSURL URLWithString:call.arguments[@"url"]]; - [FIRDynamicLinkComponents shortenURL:url - options:options - completion:[self createShortLinkCompletion:result]]; - } else if ([@"FirebaseDynamicLinks#getInitialLink" isEqualToString:call.method]) { - _initiated = YES; - NSMutableDictionary *dict = [self getInitialLink]; - if (dict == nil && self.flutterError) { - result(self.flutterError); - } else { - result(dict); - } - } else if ([@"FirebaseDynamicLinks#getDynamicLink" isEqualToString:call.method]) { - NSURL *shortLink = [NSURL URLWithString:call.arguments[@"url"]]; - FIRDynamicLinkUniversalLinkHandler completion = - ^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { - if (error) { - result(getFlutterError(error)); - } else { - result(getDictionaryFromDynamicLink(dynamicLink)); - } - }; - [[FIRDynamicLinks dynamicLinks] handleUniversalLink:shortLink completion:completion]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (NSMutableDictionary *)getInitialLink { - return getDictionaryFromDynamicLink(_initialLink); -} - -- (void)onDeepLinkResult:(FIRDynamicLink *_Nullable)dynamicLink error:(NSError *_Nullable)error { - if (_initiated) { - if (error) { - FlutterError *flutterError = getFlutterError(error); - [_channel invokeMethod:@"onLinkError" arguments:getDictionaryFromFlutterError(flutterError)]; - } else { - NSMutableDictionary *dictionary = getDictionaryFromDynamicLink(dynamicLink); - [_channel invokeMethod:@"onLinkSuccess" arguments:dictionary]; - } - } else { - if (error) { - _flutterError = getFlutterError(error); - } else if (dynamicLink.url != nil || _initialLink == nil) { - // We'd like to overwrite initial link only if it's - // the first time or if we overwrite it with url that is not nil - _initialLink = dynamicLink; - } - } -} - -- (void)checkForDynamicLink:(NSURL *)url { - FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; - if (dynamicLink) { - [self onDeepLinkResult:dynamicLink error:nil]; - } -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options { - [self checkForDynamicLink:url]; - // Results of this are ORed and NO doesn't affect other delegate interceptors' result. - return NO; -} - -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { - [self checkForDynamicLink:url]; - // Results of this are ORed and NO doesn't affect other delegate interceptors' result. - return NO; -} - -- (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(nonnull void (^)(NSArray *_Nullable))restorationHandler { - __block BOOL retried = NO; - void (^completionBlock)(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error); - void (^__block __weak weakCompletionBlock)(FIRDynamicLink *_Nullable dynamicLink, - NSError *_Nullable error); - weakCompletionBlock = completionBlock = ^(FIRDynamicLink *_Nullable dynamicLink, - NSError *_Nullable error) { - if (!error && dynamicLink && dynamicLink.url) { - [self onDeepLinkResult:dynamicLink error:nil]; - } - - // Per Apple Tech Support, a network failure could occur when returning from background on - // iOS 12. https://github.com/AFNetworking/AFNetworking/issues/4279#issuecomment-447108981 So - // we'll retry the request once - if (error && !retried && [NSPOSIXErrorDomain isEqualToString:error.domain] && - error.code == 53) { - retried = YES; - [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL - completion:weakCompletionBlock]; - } - // We could send this to Dart and maybe have a onDynamicLinkError stream but there's also - // a good chance the `userActivity.webpageURL` might not be for a Firebase dynamic link, - // which needs consideration - so we'll log this for now, logging will get picked up by - // Crashlytics automatically if its integrated. - if (error) - NSLog( - @"FLTFirebaseDynamicLinks: Unknown error occurred when attempting to handle a universal " - @"link: %@", - error); - }; - - [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL - completion:completionBlock]; - - // Results of this are ORed and NO doesn't affect other delegate interceptors' result. - return NO; -} - -- (FIRDynamicLinkShortenerCompletion)createShortLinkCompletion:(FlutterResult)result { - return ^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, NSError *_Nullable error) { - if (error) { - result(getFlutterError(error)); - } else { - if (warnings == nil) { - warnings = [NSMutableArray array]; - } - result(@{@"url" : [shortURL absoluteString], @"warnings" : warnings}); - } - }; -} - -- (FIRDynamicLinkComponentsOptions *)setupOptions:(NSDictionary *)arguments { - FIRDynamicLinkComponentsOptions *options; - if (![arguments[@"dynamicLinkParametersOptions"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"dynamicLinkParametersOptions"]; - - options = [FIRDynamicLinkComponentsOptions options]; - - NSNumber *shortDynamicLinkPathLength = params[@"shortDynamicLinkPathLength"]; - if (![shortDynamicLinkPathLength isEqual:[NSNull null]]) { - switch (shortDynamicLinkPathLength.intValue) { - case 0: - options.pathLength = FIRShortDynamicLinkPathLengthUnguessable; - break; - case 1: - options.pathLength = FIRShortDynamicLinkPathLengthShort; - break; - default: - break; - } - } - } - - return options; -} - -- (FIRDynamicLinkComponents *)setupParameters:(NSDictionary *)arguments { - NSURL *link = [NSURL URLWithString:arguments[@"link"]]; - NSString *uriPrefix = arguments[@"uriPrefix"]; - - FIRDynamicLinkComponents *components = [FIRDynamicLinkComponents componentsWithLink:link - domainURIPrefix:uriPrefix]; - - if (![arguments[@"androidParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"androidParameters"]; - - FIRDynamicLinkAndroidParameters *androidParams = - [FIRDynamicLinkAndroidParameters parametersWithPackageName:params[@"packageName"]]; - - NSString *fallbackUrl = params[@"fallbackUrl"]; - NSNumber *minimumVersion = params[@"minimumVersion"]; - - if (![fallbackUrl isEqual:[NSNull null]]) - androidParams.fallbackURL = [NSURL URLWithString:fallbackUrl]; - if (![minimumVersion isEqual:[NSNull null]]) - androidParams.minimumVersion = ((NSNumber *)minimumVersion).integerValue; - - components.androidParameters = androidParams; - } - - if (![arguments[@"dynamicLinkComponentsOptions"] isEqual:[NSNull null]]) { - components.options = [self setupOptions:arguments]; - } - - if (![arguments[@"googleAnalyticsParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"googleAnalyticsParameters"]; - - FIRDynamicLinkGoogleAnalyticsParameters *googleAnalyticsParameters = - [FIRDynamicLinkGoogleAnalyticsParameters parameters]; - - NSString *campaign = params[@"campaign"]; - NSString *content = params[@"content"]; - NSString *medium = params[@"medium"]; - NSString *source = params[@"source"]; - NSString *term = params[@"term"]; - - if (![campaign isEqual:[NSNull null]]) googleAnalyticsParameters.campaign = campaign; - if (![content isEqual:[NSNull null]]) googleAnalyticsParameters.content = content; - if (![medium isEqual:[NSNull null]]) googleAnalyticsParameters.medium = medium; - if (![source isEqual:[NSNull null]]) googleAnalyticsParameters.source = source; - if (![term isEqual:[NSNull null]]) googleAnalyticsParameters.term = term; - - components.analyticsParameters = googleAnalyticsParameters; - } - - if (![arguments[@"iosParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"iosParameters"]; - - FIRDynamicLinkIOSParameters *iosParameters = - [FIRDynamicLinkIOSParameters parametersWithBundleID:params[@"bundleId"]]; - - NSString *appStoreID = params[@"appStoreId"]; - NSString *customScheme = params[@"customScheme"]; - NSString *fallbackURL = params[@"fallbackUrl"]; - NSString *iPadBundleID = params[@"ipadBundleId"]; - NSString *iPadFallbackURL = params[@"ipadFallbackUrl"]; - NSString *minimumAppVersion = params[@"minimumVersion"]; - - if (![appStoreID isEqual:[NSNull null]]) iosParameters.appStoreID = appStoreID; - if (![customScheme isEqual:[NSNull null]]) iosParameters.customScheme = customScheme; - if (![fallbackURL isEqual:[NSNull null]]) - iosParameters.fallbackURL = [NSURL URLWithString:fallbackURL]; - if (![iPadBundleID isEqual:[NSNull null]]) iosParameters.iPadBundleID = iPadBundleID; - if (![iPadFallbackURL isEqual:[NSNull null]]) - iosParameters.iPadFallbackURL = [NSURL URLWithString:iPadFallbackURL]; - if (![minimumAppVersion isEqual:[NSNull null]]) - iosParameters.minimumAppVersion = minimumAppVersion; - - components.iOSParameters = iosParameters; - } - - if (![arguments[@"itunesConnectAnalyticsParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"itunesConnectAnalyticsParameters"]; - - FIRDynamicLinkItunesConnectAnalyticsParameters *itunesConnectAnalyticsParameters = - [FIRDynamicLinkItunesConnectAnalyticsParameters parameters]; - - NSString *affiliateToken = params[@"affiliateToken"]; - NSString *campaignToken = params[@"campaignToken"]; - NSString *providerToken = params[@"providerToken"]; - - if (![affiliateToken isEqual:[NSNull null]]) - itunesConnectAnalyticsParameters.affiliateToken = affiliateToken; - if (![campaignToken isEqual:[NSNull null]]) - itunesConnectAnalyticsParameters.campaignToken = campaignToken; - if (![providerToken isEqual:[NSNull null]]) - itunesConnectAnalyticsParameters.providerToken = providerToken; - - components.iTunesConnectParameters = itunesConnectAnalyticsParameters; - } - - if (![arguments[@"navigationInfoParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"navigationInfoParameters"]; - - FIRDynamicLinkNavigationInfoParameters *navigationInfoParameters = - [FIRDynamicLinkNavigationInfoParameters parameters]; - - NSNumber *forcedRedirectEnabled = params[@"forcedRedirectEnabled"]; - if (![forcedRedirectEnabled isEqual:[NSNull null]]) - navigationInfoParameters.forcedRedirectEnabled = [forcedRedirectEnabled boolValue]; - - components.navigationInfoParameters = navigationInfoParameters; - } - - if (![arguments[@"socialMetaTagParameters"] isEqual:[NSNull null]]) { - NSDictionary *params = arguments[@"socialMetaTagParameters"]; - - FIRDynamicLinkSocialMetaTagParameters *socialMetaTagParameters = - [FIRDynamicLinkSocialMetaTagParameters parameters]; - - NSString *descriptionText = params[@"description"]; - NSString *imageURL = params[@"imageUrl"]; - NSString *title = params[@"title"]; - - if (![descriptionText isEqual:[NSNull null]]) - socialMetaTagParameters.descriptionText = descriptionText; - if (![imageURL isEqual:[NSNull null]]) - socialMetaTagParameters.imageURL = [NSURL URLWithString:imageURL]; - if (![title isEqual:[NSNull null]]) socialMetaTagParameters.title = title; - - components.socialMetaTagParameters = socialMetaTagParameters; - } - - return components; -} - -@end diff --git a/packages/firebase_dynamic_links/lib/firebase_dynamic_links.dart b/packages/firebase_dynamic_links/lib/firebase_dynamic_links.dart deleted file mode 100644 index e5953b1b37e3..000000000000 --- a/packages/firebase_dynamic_links/lib/firebase_dynamic_links.dart +++ /dev/null @@ -1,14 +0,0 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -library firebase_dynamic_links; - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -part 'src/dynamic_link_parameters.dart'; -part 'src/firebase_dynamic_links.dart'; diff --git a/packages/firebase_dynamic_links/lib/src/dynamic_link_parameters.dart b/packages/firebase_dynamic_links/lib/src/dynamic_link_parameters.dart deleted file mode 100644 index 5b9b69949755..000000000000 --- a/packages/firebase_dynamic_links/lib/src/dynamic_link_parameters.dart +++ /dev/null @@ -1,325 +0,0 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -part of firebase_dynamic_links; - -/// The class used for Dynamic Link URL generation. -/// -/// Supports creation of short and long Dynamic Link URLs. Short URLs will have -/// a domain and a randomized path. Long URLs will have a domain and a query -/// that contains all of the Dynamic Link parameters. -class DynamicLinkParameters { - DynamicLinkParameters({ - this.androidParameters, - required this.uriPrefix, - this.dynamicLinkParametersOptions, - this.googleAnalyticsParameters, - this.iosParameters, - this.itunesConnectAnalyticsParameters, - required this.link, - this.navigationInfoParameters, - this.socialMetaTagParameters, - }); - - /// Android parameters for a generated Dynamic Link URL. - final AndroidParameters? androidParameters; - - /// Domain URI Prefix of your App. - // This value must be your assigned domain from the Firebase console. - // (e.g. https://xyz.page.link) - // - // The domain URI prefix must start with a valid HTTPS scheme (https://). - final String uriPrefix; - - /// Defines behavior for generating Dynamic Link URLs. - final DynamicLinkParametersOptions? dynamicLinkParametersOptions; - - /// Analytics parameters for a generated Dynamic Link URL. - final GoogleAnalyticsParameters? googleAnalyticsParameters; - - /// iOS parameters for a generated Dynamic Link URL. - final IosParameters? iosParameters; - - /// iTunes Connect parameters for a generated Dynamic Link URL. - final ItunesConnectAnalyticsParameters? itunesConnectAnalyticsParameters; - - /// The link the target app will open. - /// - /// You can specify any URL the app can handle, such as a link to the app’s - /// content, or a URL that initiates some app-specific logic such as crediting - /// the user with a coupon, or displaying a specific welcome screen. - /// This link must be a well-formatted URL, be properly URL-encoded, and use - /// the HTTP or HTTPS scheme. - final Uri link; - - /// Navigation Info parameters for a generated Dynamic Link URL. - final NavigationInfoParameters? navigationInfoParameters; - - /// Social Meta Tag parameters for a generated Dynamic Link URL. - final SocialMetaTagParameters? socialMetaTagParameters; - - /// Shortens a Dynamic Link URL. - /// - /// This method may be used for shortening a custom URL that was not generated - /// using [DynamicLinkParameters]. - static Future shortenUrl(Uri url, - [DynamicLinkParametersOptions? options]) async { - final Map? reply = await FirebaseDynamicLinks.channel - .invokeMapMethod( - 'DynamicLinkParameters#shortenUrl', { - 'url': url.toString(), - 'dynamicLinkParametersOptions': options?._data, - }); - return _parseShortLink(reply!); - } - - Map get _data => { - 'androidParameters': androidParameters?._data, - 'uriPrefix': uriPrefix, - 'dynamicLinkParametersOptions': dynamicLinkParametersOptions?._data, - 'googleAnalyticsParameters': googleAnalyticsParameters?._data, - 'iosParameters': iosParameters?._data, - 'itunesConnectAnalyticsParameters': - itunesConnectAnalyticsParameters?._data, - 'link': link.toString(), - 'navigationInfoParameters': navigationInfoParameters?._data, - 'socialMetaTagParameters': socialMetaTagParameters?._data, - }; - - /// Generate a long Dynamic Link URL. - Future buildUrl() async { - final String? url = await FirebaseDynamicLinks.channel - .invokeMethod('DynamicLinkParameters#buildUrl', _data); - return Uri.parse(url!); - } - - /// Generate a short Dynamic Link. - Future buildShortLink() async { - final Map? reply = await FirebaseDynamicLinks.channel - .invokeMapMethod( - 'DynamicLinkParameters#buildShortLink', _data); - return _parseShortLink(reply!); - } - - static ShortDynamicLink _parseShortLink(Map reply) { - final List? warnings = reply['warnings']; - return ShortDynamicLink._(Uri.parse(reply['url']), warnings?.cast()); - } -} - -/// Response from creating a short dynamic link with [DynamicLinkParameters]. -class ShortDynamicLink { - ShortDynamicLink._(this.shortUrl, this.warnings); - - /// Short url value. - final Uri shortUrl; - - /// Information about potential warnings on link creation. - final List? warnings; -} - -/// The Dynamic Link Android parameters. -class AndroidParameters { - AndroidParameters( - {this.fallbackUrl, this.minimumVersion, required this.packageName}); - - /// The link to open when the app isn’t installed. - /// - /// Specify this to do something other than install the app from the Play - /// Store when the app isn’t installed, such as open the mobile web version of - /// the content, or display a promotional page for the app. - final Uri? fallbackUrl; - - /// The version of the minimum version of your app that can open the link. - /// - /// If the installed app is an older version, the user is taken to the Play - /// Store to upgrade the app. - final int? minimumVersion; - - /// The Android app’s package name. - final String packageName; - - Map get _data => { - 'fallbackUrl': fallbackUrl?.toString(), - 'minimumVersion': minimumVersion, - 'packageName': packageName, - }; -} - -/// For specifying length for short Dynamic Links. -enum ShortDynamicLinkPathLength { unguessable, short } - -/// Options class for defining how Dynamic Link URLs are generated. -class DynamicLinkParametersOptions { - DynamicLinkParametersOptions({this.shortDynamicLinkPathLength}); - - /// Specifies the length of the path component of a short Dynamic Link. - final ShortDynamicLinkPathLength? shortDynamicLinkPathLength; - - Map get _data => { - 'shortDynamicLinkPathLength': shortDynamicLinkPathLength?.index, - }; -} - -/// The Dynamic Link analytics parameters. -class GoogleAnalyticsParameters { - GoogleAnalyticsParameters({ - required String this.campaign, - this.content, - required String this.medium, - required String this.source, - this.term, - }); - - GoogleAnalyticsParameters.empty() - : campaign = null, - content = null, - medium = null, - source = null, - term = null; - - /// The utm_campaign analytics parameter. - final String? campaign; - - /// The utm_content analytics parameter. - final String? content; - - /// The utm_medium analytics parameter. - final String? medium; - - /// The utm_source analytics parameter. - final String? source; - - /// The utm_term analytics parameter. - final String? term; - - Map get _data => { - 'campaign': campaign, - 'content': content, - 'medium': medium, - 'source': source, - 'term': term, - }; -} - -/// The Dynamic Link iOS parameters. -class IosParameters { - IosParameters({ - this.appStoreId, - required this.bundleId, - this.customScheme, - this.fallbackUrl, - this.ipadBundleId, - this.ipadFallbackUrl, - this.minimumVersion, - }); - - /// The appStore ID of the iOS app in AppStore. - final String? appStoreId; - - /// The bundle ID of the iOS app to use to open the link. - final String bundleId; - - /// The target app’s custom URL scheme. - /// - /// Defined to be something other than the app’s bundle ID. - final String? customScheme; - - /// The link to open when the app isn’t installed. - /// - /// Specify this to do something other than install the app from the App Store - /// when the app isn’t installed, such as open the mobile web version of the - /// content, or display a promotional page for the app. - final Uri? fallbackUrl; - - /// The bundle ID of the iOS app to use on iPads to open the link. - /// - /// This is only required if there are separate iPhone and iPad applications. - final String? ipadBundleId; - - /// The link to open on iPads when the app isn’t installed. - /// - /// Specify this to do something other than install the app from the App Store - /// when the app isn’t installed, such as open the web version of the content, - /// or display a promotional page for the app. - final Uri? ipadFallbackUrl; - - /// The the minimum version of your app that can open the link. - /// - /// It is app’s developer responsibility to open AppStore when received link - /// declares higher [minimumVersion] than currently installed. - final String? minimumVersion; - - Map get _data => { - 'appStoreId': appStoreId, - 'bundleId': bundleId, - 'customScheme': customScheme, - 'fallbackUrl': fallbackUrl?.toString(), - 'ipadBundleId': ipadBundleId, - 'ipadFallbackUrl': ipadFallbackUrl?.toString(), - 'minimumVersion': minimumVersion, - }; -} - -/// The Dynamic Link iTunes Connect parameters. -class ItunesConnectAnalyticsParameters { - ItunesConnectAnalyticsParameters( - {this.affiliateToken, this.campaignToken, this.providerToken}); - - /// The iTunes Connect affiliate token. - final String? affiliateToken; - - /// The iTunes Connect campaign token. - final String? campaignToken; - - /// The iTunes Connect provider token. - final String? providerToken; - - Map get _data => { - 'affiliateToken': affiliateToken, - 'campaignToken': campaignToken, - 'providerToken': providerToken, - }; -} - -/// Options class for defining navigation behavior of the Dynamic Link. -class NavigationInfoParameters { - NavigationInfoParameters({this.forcedRedirectEnabled}); - - /// Whether forced non-interactive redirect it to be used. - /// - /// Forced non-interactive redirect occurs when link is tapped on mobile - /// device. - /// - /// Default behavior is to disable force redirect and show interstitial page - /// where user tap will initiate navigation to the App (or AppStore if not - /// installed). Disabled force redirect normally improves reliability of the - /// click. - final bool? forcedRedirectEnabled; - - Map get _data => { - 'forcedRedirectEnabled': forcedRedirectEnabled, - }; -} - -/// The Dynamic Link Social Meta Tag parameters. -class SocialMetaTagParameters { - SocialMetaTagParameters({this.description, this.imageUrl, this.title}); - - /// The description to use when the Dynamic Link is shared in a social post. - final String? description; - - /// The URL to an image related to this link. - final Uri? imageUrl; - - /// The title to use when the Dynamic Link is shared in a social post. - final String? title; - - Map get _data => { - 'description': description, - 'imageUrl': imageUrl?.toString(), - 'title': title, - }; -} diff --git a/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart b/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart deleted file mode 100644 index 02a296129106..000000000000 --- a/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart +++ /dev/null @@ -1,164 +0,0 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -part of firebase_dynamic_links; - -typedef OnLinkSuccessCallback = Future Function( - PendingDynamicLinkData? linkData); -typedef OnLinkErrorCallback = Future Function( - OnLinkErrorException error); - -/// Firebase Dynamic Links API. -/// -/// You can get an instance by calling [FirebaseDynamicLinks.instance]. -class FirebaseDynamicLinks { - FirebaseDynamicLinks._(); - - @visibleForTesting - static const MethodChannel channel = - MethodChannel('plugins.flutter.io/firebase_dynamic_links'); - - /// Singleton of [FirebaseDynamicLinks]. - static final FirebaseDynamicLinks instance = FirebaseDynamicLinks._(); - - OnLinkSuccessCallback? _onLinkSuccess; - OnLinkErrorCallback? _onLinkError; - - /// Attempts to retrieve the dynamic link which launched the app. - /// - /// This method always returns a Future. That Future completes to null if - /// there is no pending dynamic link or any call to this method after the - /// the first attempt. - Future getInitialLink() async { - final Map? linkData = - await channel.invokeMapMethod( - 'FirebaseDynamicLinks#getInitialLink'); - return getPendingDynamicLinkDataFromMap(linkData); - } - - Future getDynamicLink(Uri url) async { - final Map? linkData = await FirebaseDynamicLinks.channel - .invokeMapMethod('FirebaseDynamicLinks#getDynamicLink', - {'url': url.toString()}); - return getPendingDynamicLinkDataFromMap(linkData); - } - - PendingDynamicLinkData? getPendingDynamicLinkDataFromMap( - Map? linkData) { - if (linkData == null) return null; - - final link = linkData['link']; - if (link == null) return null; - - PendingDynamicLinkDataAndroid? androidData; - if (linkData['android'] != null) { - final Map data = linkData['android']; - androidData = PendingDynamicLinkDataAndroid._( - data['clickTimestamp'], - data['minimumVersion'], - ); - } - - PendingDynamicLinkDataIOS? iosData; - if (linkData['ios'] != null) { - final Map data = linkData['ios']; - iosData = PendingDynamicLinkDataIOS._(data['minimumVersion']); - } - - return PendingDynamicLinkData._( - Uri.parse(link), - androidData, - iosData, - ); - } - - /// Configures onLink listeners: it has two methods for success and failure. - void onLink({ - OnLinkSuccessCallback? onSuccess, - OnLinkErrorCallback? onError, - }) { - _onLinkSuccess = onSuccess; - _onLinkError = onError; - channel.setMethodCallHandler(_handleMethod); - } - - Future _handleMethod(MethodCall call) async { - switch (call.method) { - case 'onLinkSuccess': - PendingDynamicLinkData? linkData; - if (call.arguments != null) { - final Map? data = - call.arguments.cast(); - linkData = getPendingDynamicLinkDataFromMap(data); - } - return _onLinkSuccess!(linkData); - case 'onLinkError': - final Map data = - call.arguments.cast(); - final OnLinkErrorException e = OnLinkErrorException._( - data['code'], data['message'], data['details']); - return _onLinkError!(e); - } - } -} - -/// Provides data from received dynamic link. -class PendingDynamicLinkData { - PendingDynamicLinkData._(this.link, this.android, this.ios); - - /// Provides Android specific data from received dynamic link. - /// - /// Can be null if [link] equals null or dynamic link was not received on an - /// Android device. - final PendingDynamicLinkDataAndroid? android; - - /// Provides iOS specific data from received dynamic link. - /// - /// Can be null if [link] equals null or dynamic link was not received on an - /// iOS device. - final PendingDynamicLinkDataIOS? ios; - - /// Deep link parameter of the dynamic link. - final Uri link; -} - -/// Provides android specific data from received dynamic link. -class PendingDynamicLinkDataAndroid { - PendingDynamicLinkDataAndroid._( - this.clickTimestamp, - this.minimumVersion, - ); - - /// The time the user clicked on the dynamic link. - /// - /// Equals the number of milliseconds that have elapsed since January 1, 1970. - final int? clickTimestamp; - - /// The minimum version of your app that can open the link. - /// - /// The minimum Android app version requested to process the dynamic link that - /// can be compared directly with versionCode. - /// - /// If the installed app is an older version, the user is taken to the Play - /// Store to upgrade the app. - final int? minimumVersion; -} - -/// Provides iOS specific data from received dynamic link. -class PendingDynamicLinkDataIOS { - PendingDynamicLinkDataIOS._(this.minimumVersion); - - /// The minimum version of your app that can open the link. - /// - /// It is app developer's responsibility to open AppStore when received link - /// declares higher [minimumVersion] than currently installed. - final String? minimumVersion; -} - -/// This object is returned by the handler when an error occurs. -class OnLinkErrorException extends PlatformException { - OnLinkErrorException._(String code, String? message, dynamic details) - : super(code: code, message: message, details: details); -} diff --git a/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart b/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart deleted file mode 100644 index 8e193cd8c2a1..000000000000 --- a/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart +++ /dev/null @@ -1,675 +0,0 @@ -// ignore_for_file: require_trailing_commas -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:firebase_dynamic_links/firebase_dynamic_links.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$FirebaseDynamicLinks', () { - final List log = []; - - setUp(() { - FirebaseDynamicLinks.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - final Map returnUrl = { - 'url': 'google.com', - 'warnings': ['This is only a test link'], - }; - switch (methodCall.method) { - case 'DynamicLinkParameters#buildUrl': - return 'google.com'; - case 'DynamicLinkParameters#buildShortLink': - return returnUrl; - case 'DynamicLinkParameters#shortenUrl': - return returnUrl; - case 'FirebaseDynamicLinks#getInitialLink': - return { - 'link': 'https://google.com', - 'android': { - 'clickTimestamp': 1234567, - 'minimumVersion': 12, - }, - 'ios': { - 'minimumVersion': 'Version 12', - }, - }; - case 'FirebaseDynamicLinks#getDynamicLink': - return { - 'link': 'https://google.com', - }; - default: - return null; - } - }); - log.clear(); - }); - - group('getInitialLink', () { - test('link can be parsed', () async { - final PendingDynamicLinkData? data = - await FirebaseDynamicLinks.instance.getInitialLink(); - - expect(data!.link, Uri.parse('https://google.com')); - - expect(data.android!.clickTimestamp, 1234567); - expect(data.android!.minimumVersion, 12); - - expect(data.ios!.minimumVersion, 'Version 12'); - - expect(log, [ - isMethodCall( - 'FirebaseDynamicLinks#getInitialLink', - arguments: null, - ) - ]); - }); - - // Both iOS FIRDynamicLink.url and android PendingDynamicLinkData.getUrl() - // might return null link. In such a case we want to ignore the deep-link. - test('for null link, returns null', () async { - FirebaseDynamicLinks.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'FirebaseDynamicLinks#getInitialLink': - return { - 'link': null, - 'android': { - 'clickTimestamp': 1234567, - 'minimumVersion': 12, - }, - 'ios': { - 'minimumVersion': 'Version 12', - }, - }; - default: - return null; - } - }); - - final PendingDynamicLinkData? data = - await FirebaseDynamicLinks.instance.getInitialLink(); - - expect(data, isNull); - - expect(log, [ - isMethodCall( - 'FirebaseDynamicLinks#getInitialLink', - arguments: null, - ) - ]); - }); - - test('for null result, returns null', () async { - FirebaseDynamicLinks.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'FirebaseDynamicLinks#getInitialLink': - return null; - default: - return null; - } - }); - - final PendingDynamicLinkData? data = - await FirebaseDynamicLinks.instance.getInitialLink(); - - expect(data, isNull); - - expect(log, [ - isMethodCall( - 'FirebaseDynamicLinks#getInitialLink', - arguments: null, - ) - ]); - }); - }); - - test('getDynamicLink', () async { - final Uri argument = Uri.parse('short-link'); - final PendingDynamicLinkData? data = - await FirebaseDynamicLinks.instance.getDynamicLink(argument); - - expect(data!.link.host, 'google.com'); - - expect(log, [ - isMethodCall('FirebaseDynamicLinks#getDynamicLink', - arguments: { - 'url': argument.toString(), - }) - ]); - }); - - group('$DynamicLinkParameters', () { - test('shortenUrl', () async { - final Uri url = Uri.parse('google.com'); - final DynamicLinkParametersOptions options = - DynamicLinkParametersOptions( - shortDynamicLinkPathLength: - ShortDynamicLinkPathLength.unguessable); - - await DynamicLinkParameters.shortenUrl(url, options); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#shortenUrl', - arguments: { - 'url': url.toString(), - 'dynamicLinkParametersOptions': { - 'shortDynamicLinkPathLength': - ShortDynamicLinkPathLength.unguessable.index, - }, - }, - ), - ]); - }); - - test('$AndroidParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - androidParameters: AndroidParameters( - fallbackUrl: Uri.parse('test-url'), - minimumVersion: 1, - packageName: 'test-package', - ), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': { - 'fallbackUrl': 'test-url', - 'minimumVersion': 1, - 'packageName': 'test-package', - }, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': { - 'fallbackUrl': 'test-url', - 'minimumVersion': 1, - 'packageName': 'test-package', - }, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$DynamicLinkParametersOptions', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - dynamicLinkParametersOptions: DynamicLinkParametersOptions( - shortDynamicLinkPathLength: ShortDynamicLinkPathLength.short), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': { - 'shortDynamicLinkPathLength': - ShortDynamicLinkPathLength.short.index, - }, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': { - 'shortDynamicLinkPathLength': - ShortDynamicLinkPathLength.short.index, - }, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$ShortDynamicLinkPathLength', () { - expect(ShortDynamicLinkPathLength.unguessable.index, 0); - expect(ShortDynamicLinkPathLength.short.index, 1); - }); - - test('$GoogleAnalyticsParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - googleAnalyticsParameters: GoogleAnalyticsParameters( - campaign: 'where', - content: 'is', - medium: 'my', - source: 'cat', - term: 'friend', - ), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': { - 'campaign': 'where', - 'content': 'is', - 'medium': 'my', - 'source': 'cat', - 'term': 'friend', - }, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': { - 'campaign': 'where', - 'content': 'is', - 'medium': 'my', - 'source': 'cat', - 'term': 'friend', - }, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$IosParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - iosParameters: IosParameters( - appStoreId: 'is', - bundleId: 'this', - customScheme: 'the', - fallbackUrl: Uri.parse('place'), - ipadBundleId: 'to', - ipadFallbackUrl: Uri.parse('find'), - minimumVersion: 'potatoes', - ), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': { - 'appStoreId': 'is', - 'bundleId': 'this', - 'customScheme': 'the', - 'fallbackUrl': 'place', - 'ipadBundleId': 'to', - 'ipadFallbackUrl': 'find', - 'minimumVersion': 'potatoes', - }, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': { - 'appStoreId': 'is', - 'bundleId': 'this', - 'customScheme': 'the', - 'fallbackUrl': 'place', - 'ipadBundleId': 'to', - 'ipadFallbackUrl': 'find', - 'minimumVersion': 'potatoes', - }, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$ItunesConnectAnalyticsParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - itunesConnectAnalyticsParameters: ItunesConnectAnalyticsParameters( - affiliateToken: 'hello', - campaignToken: 'mister', - providerToken: 'rose', - ), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': { - 'affiliateToken': 'hello', - 'campaignToken': 'mister', - 'providerToken': 'rose', - }, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': { - 'affiliateToken': 'hello', - 'campaignToken': 'mister', - 'providerToken': 'rose', - }, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$NavigationInfoParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - navigationInfoParameters: - NavigationInfoParameters(forcedRedirectEnabled: true), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': { - 'forcedRedirectEnabled': true, - }, - 'socialMetaTagParameters': null, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': { - 'forcedRedirectEnabled': true, - }, - 'socialMetaTagParameters': null, - }, - ), - ]); - }); - - test('$SocialMetaTagParameters', () async { - final DynamicLinkParameters components = DynamicLinkParameters( - uriPrefix: 'https://test-domain/', - link: Uri.parse('test-link.com'), - socialMetaTagParameters: SocialMetaTagParameters( - description: 'describe', - imageUrl: Uri.parse('thisimage'), - title: 'bro', - ), - ); - - await components.buildUrl(); - await components.buildShortLink(); - - expect(log, [ - isMethodCall( - 'DynamicLinkParameters#buildUrl', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': { - 'description': 'describe', - 'imageUrl': 'thisimage', - 'title': 'bro', - }, - }, - ), - isMethodCall( - 'DynamicLinkParameters#buildShortLink', - arguments: { - 'androidParameters': null, - 'uriPrefix': 'https://test-domain/', - 'dynamicLinkParametersOptions': null, - 'googleAnalyticsParameters': null, - 'iosParameters': null, - 'itunesConnectAnalyticsParameters': null, - 'link': 'test-link.com', - 'navigationInfoParameters': null, - 'socialMetaTagParameters': { - 'description': 'describe', - 'imageUrl': 'thisimage', - 'title': 'bro', - }, - }, - ), - ]); - }); - }); - - group('onLink', () { - OnLinkSuccessCallback? onSuccess; - OnLinkErrorCallback? onError; - final List successLog = - []; - final List errorLog = []; - setUp(() { - onSuccess = (linkData) async { - successLog.add(linkData); - }; - onError = (error) async { - errorLog.add(error); - }; - successLog.clear(); - errorLog.clear(); - }); - - Future callMethodHandler(String method, dynamic arguments) { - const channel = FirebaseDynamicLinks.channel; - final methodCall = MethodCall(method, arguments); - final data = channel.codec.encodeMethodCall(methodCall); - final Completer completer = Completer(); - channel.binaryMessenger.handlePlatformMessage( - channel.name, - data, - (data) { - completer.complete(null); - }, - ); - return completer.future; - } - - test('onSuccess', () async { - FirebaseDynamicLinks.instance - .onLink(onSuccess: onSuccess, onError: onError); - await callMethodHandler('onLinkSuccess', { - 'link': 'https://google.com', - 'android': { - 'clickTimestamp': 1234567, - 'minimumVersion': 12, - }, - 'ios': { - 'minimumVersion': 'Version 12', - }, - }); - - expect(successLog, hasLength(1)); - expect(errorLog, hasLength(0)); - final success = successLog[0]!; - - expect(success.link, Uri.parse('https://google.com')); - - expect(success.android!.clickTimestamp, 1234567); - expect(success.android!.minimumVersion, 12); - - expect(success.ios!.minimumVersion, 'Version 12'); - }); - - test('onSuccess with null link', () async { - FirebaseDynamicLinks.instance - .onLink(onSuccess: onSuccess, onError: onError); - await callMethodHandler('onLinkSuccess', { - 'link': null, - 'android': { - 'clickTimestamp': 1234567, - 'minimumVersion': 12, - }, - 'ios': { - 'minimumVersion': 'Version 12', - }, - }); - - expect(successLog, hasLength(1)); - expect(errorLog, hasLength(0)); - final success = successLog[0]; - - expect(success, isNull); - }); - - test('onSuccess with null', () async { - FirebaseDynamicLinks.instance - .onLink(onSuccess: onSuccess, onError: onError); - await callMethodHandler('onLinkSuccess', null); - - expect(successLog, hasLength(1)); - expect(errorLog, hasLength(0)); - final success = successLog[0]; - - expect(success, isNull); - }); - - test('onError', () async { - FirebaseDynamicLinks.instance - .onLink(onSuccess: onSuccess, onError: onError); - await callMethodHandler('onLinkError', { - 'code': 'code', - 'message': 'message', - 'details': 'details', - }); - - expect(successLog, hasLength(0)); - expect(errorLog, hasLength(1)); - final failure = errorLog[0]; - expect(failure.code, 'code'); - expect(failure.message, 'message'); - expect(failure.details, 'details'); - }); - }); - }); -} diff --git a/website/dictionary.js b/website/dictionary.js index ad393d6380a5..f092941e6172 100644 --- a/website/dictionary.js +++ b/website/dictionary.js @@ -14,6 +14,7 @@ module.exports = [ 'acs', 'alloc', 'analytics', + 'applinks', 'apns', 'aps', 'async', @@ -55,6 +56,7 @@ module.exports = [ 'href', 'html', 'http', + 'https', 'img', 'init', 'installable', @@ -87,8 +89,10 @@ module.exports = [ 'untampered', 'untrusted', 'url', + 'uri', 'verifications', 'web.firebase_cdn', 'xml', 'yaml', + 'programmatically', ];