diff --git a/packages/firebase_dynamic_links/CHANGELOG.md b/packages/firebase_dynamic_links/CHANGELOG.md index e6f491de59b0..04e18d9e9815 100644 --- a/packages/firebase_dynamic_links/CHANGELOG.md +++ b/packages/firebase_dynamic_links/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.5.0 + +* **Breaking change**. Changed architecture and method names to be able to differentiate between +the dynamic link which opened the app and links clicked during app execution (active and background). +`retrieveDynamicLink` has been replaced with two different functions: +- `getInitialLink` a future to retrieve the link that opened the app +- `onLink` a callback to listen to links opened while the app is active or in background + ## 0.4.0+6 * Update google-services Android gradle plugin to 4.3.0 in documentation and examples. diff --git a/packages/firebase_dynamic_links/README.md b/packages/firebase_dynamic_links/README.md index b3250deb6a56..e5b755d6f421 100644 --- a/packages/firebase_dynamic_links/README.md +++ b/packages/firebase_dynamic_links/README.md @@ -97,7 +97,8 @@ Receiving dynamic links on *iOS* requires a couple more steps than *Android*. If applinks:YOUR_SUBDOMAIN.page.link ``` -4. To receive a dynamic link, call the `retrieveDynamicLink()` method from `FirebaseDynamicLinks`: +4. 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() { @@ -117,16 +118,30 @@ class MyHomeWidgetState extends State { @override void initState() { super.initState(); - _retrieveDynamicLink(); + this.initDynamicLinks(); } - Future _retrieveDynamicLink() async { - final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.retrieveDynamicLink(); + void initDynamicLinks() async { + final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink(); final Uri deepLink = data?.link; if (deepLink != null) { - Navigator.pushNamed(context, deepLink.path); // deeplink.path == '/helloworld' + Navigator.pushNamed(context, deepLink.path); } + + 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); + } + ); } . . @@ -134,7 +149,7 @@ class MyHomeWidgetState extends State { } ``` -If your app did not open from a dynamic link, `retrieveDynamicLink()` will return `null`. +If your app did not open from a dynamic link, `getInitialLink()` will return `null`. ## Getting Started 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 index c4ed3a5903f6..5cc4debaa404 100644 --- 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 @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; 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; @@ -14,7 +15,7 @@ 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; +import io.flutter.plugin.common.PluginRegistry.NewIntentListener; import io.flutter.plugin.common.PluginRegistry.Registrar; import java.util.ArrayList; import java.util.HashMap; @@ -22,30 +23,53 @@ import java.util.Map; /** FirebaseDynamicLinksPlugin */ -public class FirebaseDynamicLinksPlugin implements MethodCallHandler { - private Registrar registrar; - private Intent latestIntent; +public class FirebaseDynamicLinksPlugin implements MethodCallHandler, NewIntentListener { + private final Registrar registrar; + private final MethodChannel channel; - private FirebaseDynamicLinksPlugin(Registrar registrar) { + private FirebaseDynamicLinksPlugin(Registrar registrar, MethodChannel channel) { this.registrar = registrar; - if (registrar.activity() != null) { - latestIntent = registrar.activity().getIntent(); - } + this.channel = channel; + } - registrar.addNewIntentListener( - new PluginRegistry.NewIntentListener() { - @Override - public boolean onNewIntent(Intent intent) { - latestIntent = intent; - return false; - } - }); + @Override + public boolean onNewIntent(Intent intent) { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(intent) + .addOnSuccessListener( + registrar.activity(), + new OnSuccessListener() { + @Override + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null) { + Map dynamicLink = + getMapFromPendingDynamicLinkData(pendingDynamicLinkData); + channel.invokeMethod("onLinkSuccess", dynamicLink); + } + } + }) + .addOnFailureListener( + registrar.activity(), + new OnFailureListener() { + @Override + public void onFailure(@NonNull 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; } public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/firebase_dynamic_links"); - channel.setMethodCallHandler(new FirebaseDynamicLinksPlugin(registrar)); + final FirebaseDynamicLinksPlugin plugin = new FirebaseDynamicLinksPlugin(registrar, channel); + registrar.addNewIntentListener(plugin); + channel.setMethodCallHandler(plugin); } @Override @@ -66,8 +90,8 @@ public void onMethodCall(MethodCall call, Result result) { builder.setLongLink(url); buildShortDynamicLink(builder, call, createShortLinkListener(result)); break; - case "FirebaseDynamicLinks#retrieveDynamicLink": - handleRetrieveDynamicLink(result); + case "FirebaseDynamicLinks#getInitialLink": + handleGetInitialDynamicLink(result); break; default: result.notImplemented(); @@ -75,40 +99,38 @@ public void onMethodCall(MethodCall call, Result result) { } } - private void handleRetrieveDynamicLink(final Result result) { - if (latestIntent == null) { - result.success(null); - return; - } + private Map getMapFromPendingDynamicLinkData( + PendingDynamicLinkData pendingDynamicLinkData) { + Map dynamicLink = new HashMap<>(); + dynamicLink.put("link", pendingDynamicLinkData.getLink().toString()); + + Map androidData = new HashMap<>(); + androidData.put("clickTimestamp", pendingDynamicLinkData.getClickTimestamp()); + androidData.put("minimumVersion", pendingDynamicLinkData.getMinimumAppVersion()); + dynamicLink.put("android", androidData); + return dynamicLink; + } + + private void handleGetInitialDynamicLink(final Result result) { FirebaseDynamicLinks.getInstance() - .getDynamicLink(latestIntent) - .addOnCompleteListener( + .getDynamicLink(registrar.activity().getIntent()) + .addOnSuccessListener( registrar.activity(), - new OnCompleteListener() { + new OnSuccessListener() { @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful()) { - PendingDynamicLinkData data = task.getResult(); - if (data != null) { - Map dynamicLink = new HashMap<>(); - dynamicLink.put("link", data.getLink().toString()); - - Map androidData = new HashMap<>(); - androidData.put("clickTimestamp", data.getClickTimestamp()); - androidData.put("minimumVersion", data.getMinimumAppVersion()); - - dynamicLink.put("android", androidData); - - latestIntent = null; - result.success(dynamicLink); - return; - } + public void onSuccess(PendingDynamicLinkData pendingDynamicLinkData) { + if (pendingDynamicLinkData != null) { + Map dynamicLink = + getMapFromPendingDynamicLinkData(pendingDynamicLinkData); + result.success(dynamicLink); + return; } result.success(null); } }) .addOnFailureListener( + registrar.activity(), new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { diff --git a/packages/firebase_dynamic_links/example/lib/main.dart b/packages/firebase_dynamic_links/example/lib/main.dart index 443b79d986db..6784e2d0e2ef 100644 --- a/packages/firebase_dynamic_links/example/lib/main.dart +++ b/packages/firebase_dynamic_links/example/lib/main.dart @@ -24,7 +24,7 @@ class _MainScreen extends StatefulWidget { State createState() => _MainScreenState(); } -class _MainScreenState extends State<_MainScreen> with WidgetsBindingObserver { +class _MainScreenState extends State<_MainScreen> { String _linkMessage; bool _isCreatingLink = false; String _testString = @@ -36,31 +36,29 @@ class _MainScreenState extends State<_MainScreen> with WidgetsBindingObserver { @override void initState() { super.initState(); - _retrieveDynamicLink(); - WidgetsBinding.instance.addObserver(this); + initDynamicLinks(); } - @override - void dispose() { - super.dispose(); - WidgetsBinding.instance.removeObserver(this); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - _retrieveDynamicLink(); - } - } - - Future _retrieveDynamicLink() async { + void initDynamicLinks() async { final PendingDynamicLinkData data = - await FirebaseDynamicLinks.instance.retrieveDynamicLink(); + await FirebaseDynamicLinks.instance.getInitialLink(); final Uri deepLink = data?.link; if (deepLink != null) { Navigator.pushNamed(context, deepLink.path); } + + 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); + }); } Future _createDynamicLink(bool short) async { diff --git a/packages/firebase_dynamic_links/ios/Classes/FirebaseDynamicLinksPlugin.m b/packages/firebase_dynamic_links/ios/Classes/FirebaseDynamicLinksPlugin.m index a98b382cc09a..4ab04a4e8723 100644 --- a/packages/firebase_dynamic_links/ios/Classes/FirebaseDynamicLinksPlugin.m +++ b/packages/firebase_dynamic_links/ios/Classes/FirebaseDynamicLinksPlugin.m @@ -9,9 +9,39 @@ 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) FIRDynamicLink *dynamicLink; +@property(nonatomic, retain) FlutterMethodChannel *channel; +@property(nonatomic, retain) FIRDynamicLink *initialLink; @property(nonatomic, retain) FlutterError *flutterError; +@property(nonatomic) BOOL initiated; @end @implementation FLTFirebaseDynamicLinksPlugin @@ -19,7 +49,8 @@ + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/firebase_dynamic_links" binaryMessenger:[registrar messenger]]; - FLTFirebaseDynamicLinksPlugin *instance = [[FLTFirebaseDynamicLinksPlugin alloc] init]; + FLTFirebaseDynamicLinksPlugin *instance = + [[FLTFirebaseDynamicLinksPlugin alloc] initWithChannel:channel]; [registrar addMethodCallDelegate:instance channel:channel]; [registrar addApplicationDelegate:instance]; @@ -29,9 +60,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } } -- (instancetype)init { +- (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]; @@ -54,11 +87,11 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [FIRDynamicLinkComponents shortenURL:url options:options completion:[self createShortLinkCompletion:result]]; - } else if ([@"FirebaseDynamicLinks#retrieveDynamicLink" isEqualToString:call.method]) { - NSMutableDictionary *dict = [self retrieveDynamicLink]; + } else if ([@"FirebaseDynamicLinks#getInitialLink" isEqualToString:call.method]) { + _initiated = YES; + NSMutableDictionary *dict = [self getInitialLink]; if (dict == nil && self.flutterError) { result(self.flutterError); - self.flutterError = nil; } else { result(dict); } @@ -67,21 +100,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } -- (NSMutableDictionary *)retrieveDynamicLink { - if (_dynamicLink != nil) { - NSMutableDictionary *dynamicLink = [[NSMutableDictionary alloc] init]; - dynamicLink[@"link"] = _dynamicLink.url.absoluteString; - - NSMutableDictionary *iosData = [[NSMutableDictionary alloc] init]; - if (_dynamicLink.minimumAppVersion) { - iosData[@"minimumVersion"] = _dynamicLink.minimumAppVersion; - } - _dynamicLink = nil; - dynamicLink[@"ios"] = iosData; - return dynamicLink; - } else { - return nil; - } +- (NSMutableDictionary *)getInitialLink { + return getDictionaryFromDynamicLink(_initialLink); } - (BOOL)application:(UIApplication *)application @@ -100,27 +120,49 @@ - (BOOL)application:(UIApplication *)application - (BOOL)checkForDynamicLink:(NSURL *)url { FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; if (dynamicLink) { - if (dynamicLink.url) _dynamicLink = dynamicLink; + if (dynamicLink.url) _initialLink = dynamicLink; return YES; } return NO; } -- (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray *))restorationHandler { - usleep(50000); +- (BOOL)onLink:(NSUserActivity *)userActivity { + BOOL handled = [[FIRDynamicLinks dynamicLinks] + handleUniversalLink:userActivity.webpageURL + completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (error) { + FlutterError *flutterError = getFlutterError(error); + [self.channel invokeMethod:@"onLinkError" + arguments:getDictionaryFromFlutterError(flutterError)]; + } else { + NSMutableDictionary *dictionary = getDictionaryFromDynamicLink(dynamicLink); + [self.channel invokeMethod:@"onLinkSuccess" arguments:dictionary]; + } + }]; + return handled; +} + +- (BOOL)onInitialLink:(NSUserActivity *)userActivity { BOOL handled = [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { if (error) { self.flutterError = getFlutterError(error); } - self.dynamicLink = dynamicLink; + self.initialLink = dynamicLink; }]; return handled; } +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + if (_initiated) { + return [self onLink:userActivity]; + } + return [self onInitialLink:userActivity]; +} + - (FIRDynamicLinkShortenerCompletion)createShortLinkCompletion:(FlutterResult)result { return ^(NSURL *_Nullable shortURL, NSArray *_Nullable warnings, NSError *_Nullable error) { if (error) { diff --git a/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart b/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart index 647cb8c267d0..c247fe82bb28 100644 --- a/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart +++ b/packages/firebase_dynamic_links/lib/src/firebase_dynamic_links.dart @@ -4,6 +4,11 @@ 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]. @@ -17,16 +22,23 @@ class FirebaseDynamicLinks { /// Singleton of [FirebaseDynamicLinks]. static final FirebaseDynamicLinks instance = FirebaseDynamicLinks._(); - /// Attempts to retrieve a pending dynamic link. + 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 retrieveDynamicLink() async { + Future getInitialLink() async { final Map linkData = await channel.invokeMapMethod( - 'FirebaseDynamicLinks#retrieveDynamicLink'); + 'FirebaseDynamicLinks#getInitialLink'); + return getPendingDynamicLinkDataFromMap(linkData); + } + PendingDynamicLinkData getPendingDynamicLinkDataFromMap( + Map linkData) { if (linkData == null) return null; PendingDynamicLinkDataAndroid androidData; @@ -50,6 +62,33 @@ class FirebaseDynamicLinks { 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": + final Map data = + call.arguments.cast(); + final PendingDynamicLinkData 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. @@ -104,3 +143,13 @@ class PendingDynamicLinkDataIOS { /// declares higher [minimumVersion] than currently installed. final String minimumVersion; } + +class OnLinkErrorException { + OnLinkErrorException._(this.code, this.message, this.details); + + final String code; + + final String message; + + final dynamic details; +} diff --git a/packages/firebase_dynamic_links/pubspec.yaml b/packages/firebase_dynamic_links/pubspec.yaml index 69cfe7f4dc97..73848496a15b 100644 --- a/packages/firebase_dynamic_links/pubspec.yaml +++ b/packages/firebase_dynamic_links/pubspec.yaml @@ -1,7 +1,7 @@ name: firebase_dynamic_links description: Flutter plugin for Google Dynamic Links for Firebase, an app solution for creating and handling links across multiple platforms. -version: 0.4.0+6 +version: 0.5.0 author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_dynamic_links diff --git a/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart b/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart index acf7121c7f22..e89dfef03df3 100644 --- a/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart +++ b/packages/firebase_dynamic_links/test/firebase_dynamic_links_test.dart @@ -26,7 +26,7 @@ void main() { return returnUrl; case 'DynamicLinkParameters#shortenUrl': return returnUrl; - case 'FirebaseDynamicLinks#retrieveDynamicLink': + case 'FirebaseDynamicLinks#getInitialLink': return { 'link': 'https://google.com', 'android': { @@ -44,9 +44,9 @@ void main() { log.clear(); }); - test('retrieveDynamicLink', () async { + test('getInitialLink', () async { final PendingDynamicLinkData data = - await FirebaseDynamicLinks.instance.retrieveDynamicLink(); + await FirebaseDynamicLinks.instance.getInitialLink(); expect(data.link, Uri.parse('https://google.com')); @@ -57,7 +57,7 @@ void main() { expect(log, [ isMethodCall( - 'FirebaseDynamicLinks#retrieveDynamicLink', + 'FirebaseDynamicLinks#getInitialLink', arguments: null, ) ]);