diff --git a/packages/dynamic-links/android/src/main/java/io/invertase/firebase/dynamiclinks/ReactNativeFirebaseDynamicLinksModule.java b/packages/dynamic-links/android/src/main/java/io/invertase/firebase/dynamiclinks/ReactNativeFirebaseDynamicLinksModule.java index c9a5fdbf37..e625f42097 100644 --- a/packages/dynamic-links/android/src/main/java/io/invertase/firebase/dynamiclinks/ReactNativeFirebaseDynamicLinksModule.java +++ b/packages/dynamic-links/android/src/main/java/io/invertase/firebase/dynamiclinks/ReactNativeFirebaseDynamicLinksModule.java @@ -144,6 +144,33 @@ public void getInitialLink(Promise promise) { }); } + @ReactMethod + public void resolveLink(String link, Promise promise) { + try { + FirebaseDynamicLinks.getInstance() + .getDynamicLink(Uri.parse(link)) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + PendingDynamicLinkData linkData = task.getResult(); + // Note: link == null if link invalid, isSuccessful is only false on processing error + if (linkData != null && linkData.getLink() != null && linkData.getLink().toString() != null) { + String linkUrl = linkData.getLink().toString(); + int linkMinimumVersion = linkData.getMinimumAppVersion(); + promise.resolve(dynamicLinkToWritableMap(linkUrl, linkMinimumVersion)); + } else { + rejectPromiseWithCodeAndMessage(promise, "not-found", "Dynamic link not found"); + } + } else { + rejectPromiseWithCodeAndMessage(promise, "resolve-link-error", task.getException().getMessage()); + } + }); + } + catch (Exception e) { + // This would be very unexpected, but crashing is even less expected + rejectPromiseWithCodeAndMessage(promise, "resolve-link-error", "Unknown resolve failure"); + } + } + private WritableMap dynamicLinkToWritableMap(String url, int minVersion) { WritableMap writableMap = Arguments.createMap(); diff --git a/packages/dynamic-links/e2e/dynamicLinks.e2e.js b/packages/dynamic-links/e2e/dynamicLinks.e2e.js index 207a22656f..a35d2a0e9b 100644 --- a/packages/dynamic-links/e2e/dynamicLinks.e2e.js +++ b/packages/dynamic-links/e2e/dynamicLinks.e2e.js @@ -80,6 +80,76 @@ describe('dynamicLinks()', () => { }); }); + describe('resolveLink()', () => { + it('resolves a long link', async () => { + const link = await firebase.dynamicLinks().resolveLink(TEST_LINK2); + link.should.be.an.Object(); + link.url.should.equal('https://invertase.io/hire-us'); + should.equal(link.minimumAppVersion, null); + }); + + it('resolves a short link', async () => { + const shortLink = await firebase.dynamicLinks().buildShortLink( + { + ...baseParams, + ios: { + bundleId: 'io.invertase.testing', + minimumVersion: '123', + }, + android: { + packageName: 'com.invertase.testing', + minimumVersion: '123', + }, + }, + firebase.dynamicLinks.ShortLinkType.UNGUESSABLE, + ); + shortLink.should.be.String(); + // Unguessable links are 17 characters by definitions, add the slash: 18 chars + shortLink.length.should.be.eql(baseParams.domainUriPrefix.length + 18); + + const link = await firebase.dynamicLinks().resolveLink(shortLink); + link.should.be.an.Object(); + link.url.should.equal(baseParams.link); + // TODO: harmonize the API so that minimumAppVersion is either a number or a string + // it would be a breaking change in the API though + // On Android it's a number and iOS a String, so parseInt is used to have a single test + parseInt(link.minimumAppVersion, 10).should.equal(123); + }); + + it('throws on links that do not exist', async () => { + try { + await firebase.dynamicLinks().resolveLink(baseParams.domainUriPrefix + '/not-a-valid-link'); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.code.should.containEql('not-found'); + e.message.should.containEql('Dynamic link not found'); + return Promise.resolve(); + } + }); + + it('throws on invalid links', async () => { + try { + await firebase.dynamicLinks().resolveLink(null); + return Promise.reject(new Error('Did not throw Error.')); + } catch (e) { + e.message.should.containEql('Invalid link parameter'); + return Promise.resolve(); + } + }); + + // // The API is documented as being capable of suffering a processing failure, and we + // // handle it, but I don't know how to trigger it to validate + // it('throws on link processing error', async () => { + // try { + // await firebase.dynamicLinks().resolveLink(SOME UNKNOWN INPUT TO CAUSE PROCESSING ERROR); + // return Promise.reject(new Error('Did not throw Error.')); + // } catch (e) { + // e.code.should.containEql('resolve-link-error'); + // return Promise.resolve(); + // } + // }); + }); + ios.describe('getInitialLink()', () => { it('should return the dynamic link instance that launched the app', async () => { await device.openURL({ diff --git a/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksModule.m b/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksModule.m index f4becee643..4fbdda462c 100644 --- a/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksModule.m +++ b/packages/dynamic-links/ios/RNFBDynamicLinks/RNFBDynamicLinksModule.m @@ -182,6 +182,34 @@ - (id)init { } } +RCT_EXPORT_METHOD(resolveLink: + (NSString *) link + :(RCTPromiseResolveBlock) resolve + :(RCTPromiseRejectBlock) reject) +{ + id completion = ^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (!error && dynamicLink && dynamicLink.url) { + resolve(@{ + @"url": dynamicLink.url.absoluteString, + @"minimumAppVersion": dynamicLink.minimumAppVersion == nil ? [NSNull null] : dynamicLink.minimumAppVersion, + }); + } else if (!error) { + [RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:(NSMutableDictionary *) @{ + @"code": @"not-found", + @"message": @"Dynamic link not found" + }]; + } else { + [RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:(NSMutableDictionary *) @{ + @"code": @"resolve-link-error", + @"message":[error localizedDescription] + }]; + } +}; + + NSURL *linkURL = [NSURL URLWithString:link]; + [[FIRDynamicLinks dynamicLinks] handleUniversalLink:linkURL completion:completion]; +} + - (FIRDynamicLinkComponents *)createDynamicLinkComponents:(NSDictionary *)dynamicLinkDict { NSURL *link = [NSURL URLWithString:dynamicLinkDict[@"link"]]; FIRDynamicLinkComponents *linkComponents = [FIRDynamicLinkComponents componentsWithLink:link domainURIPrefix:dynamicLinkDict[@"domainUriPrefix"]]; diff --git a/packages/dynamic-links/lib/index.d.ts b/packages/dynamic-links/lib/index.d.ts index ac279d3aaa..cb01e42f7d 100644 --- a/packages/dynamic-links/lib/index.d.ts +++ b/packages/dynamic-links/lib/index.d.ts @@ -403,9 +403,14 @@ export namespace FirebaseDynamicLinksTypes { url: string; /** - * The minimum app version requested to process the dynamic link. + * The minimum app version (not system version) requested to process the dynamic link. + * This is retrieved from the imv= parameter of the Dynamic Link URL. * - * Returns `null` if not specified. + * If the app version of the opening app is less than the value of this property, + * then the app is expected to open AppStore to allow user to download most recent version. + * App can notify or ask the user before opening AppStore. + * + * Returns `null` if not specified * * #### Android * @@ -413,8 +418,7 @@ export namespace FirebaseDynamicLinksTypes { * * #### iOS * - * On iOS this returns a string value representing the minimum app version (not the iOS system version). If the app version of the opening app is less than the value of this property, then the app is expected to open AppStore to allow user to download most recent version. App can notify or ask the user before opening AppStore. - * + * On iOS this returns a string value representing the minimum app version (not the iOS system version). */ minimumAppVersion: number | string | null; } @@ -558,6 +562,28 @@ export namespace FirebaseDynamicLinksTypes { * @param listener The listener callback, called with Dynamic Link instances. */ onLink(listener: Function): Function; + + /** + * Resolve a given dynamic link (short or long) directly. + * + * This mimics the result of external link resolution, app open, and the DynamicLink you + * would get from {@link dynamic-links#getInitialLink} + * + * #### Example + * + * ```js + * const link = await firebase.dynamicLinks().resolveLink('https://reactnativefirebase.page.link/76adfasdf'); + * console.log('Received link with URL: ' + link.url); + * ``` + * + * Can throw error with message 'Invalid link parameter' if link parameter is null + * Can throw error with code 'not-found' if the link does not resolve + * Can throw error with code 'resolve-link-error' if there is a processing error + + * @returns the resolved Dynamic Link + * @param link The Dynamic Link URL to resolve, either short or long + */ + resolveLink(link: string): Promise; } } diff --git a/packages/dynamic-links/lib/index.js b/packages/dynamic-links/lib/index.js index 6700632586..9e9acaf62e 100644 --- a/packages/dynamic-links/lib/index.js +++ b/packages/dynamic-links/lib/index.js @@ -99,6 +99,13 @@ class FirebaseLinksModule extends FirebaseModule { subscription.remove(); }; } + + resolveLink(link) { + if (!link) { + throw new Error('firebase.dynamicLinks().resolve(*) Invalid link parameter'); + } + return this.native.resolveLink(link); + } } // import { SDK_VERSION } from '@react-native-firebase/dynamic-links'; diff --git a/packages/dynamic-links/lib/index.js.flow b/packages/dynamic-links/lib/index.js.flow index 9ad3079854..5d39e7074a 100644 --- a/packages/dynamic-links/lib/index.js.flow +++ b/packages/dynamic-links/lib/index.js.flow @@ -371,6 +371,36 @@ export interface ShortLinkType { DEFAULT: 'DEFAULT'; } +/** + * A received Dynamic Link from either `onLink` or `getInitialLink`. + */ +export interface DynamicLink { + /** + * The url of the dynamic link. + */ + url: string; + + /** + * The minimum app version (not system version) requested to process the dynamic link. + * This is retrieved from the imv= parameter of the Dynamic Link URL. + * + * If the app version of the opening app is less than the value of this property, + * then the app is expected to open AppStore to allow user to download most recent version. + * App can notify or ask the user before opening AppStore. + * + * Returns `null` if not specified + * + * #### Android + * + * On Android this returns a number value representing the apps [versionCode](https://developer.android.com/reference/android/content/pm/PackageInfo.html#versionCode). + * + * #### iOS + * + * On iOS this returns a string value representing the minimum app version (not the iOS system version). + */ + minimumAppVersion: number | string | null; +} + /** * Firebase Dynamic DynamicLinks Statics * @@ -534,6 +564,27 @@ export interface Module extends ReactNativeFirebaseModule { * @param listener The listener callback, called URL open events. */ onLink(listener: Function): Function; + + /** + * Resolve a given dynamic link (short or long) directly. + * + * This mimics the result of external link resolution, app open, and the DynamicLink you + * would get from {@link dynamic-links#getInitialLink} + * + * #### Example + * + * ```js + * const link = await firebase.dynamicLinks().resolveLink('https://invertase.io'); + * ``` + * + * Can throw error with message 'Invalid link parameter' if link parameter is null + * Can throw error with code 'not-found' if the link does not resolve + * Can throw error with code 'resolve-link-error' if there is a processing error + * + * @returns the resolved Dynamic Link URL + * @param link The Dynamic Link URL to resolve, either short or long + */ + resolveLink(link: string): Promise; } declare module '@react-native-firebase/dynamic-links' {