Skip to content

Commit

Permalink
feat(dynamic-links): API to directly resolve dynamic link (#3814)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike Diarmid <[email protected]>

[publish]
  • Loading branch information
mikehardy authored Jun 22, 2020
1 parent e716fe0 commit c43e8f7
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
70 changes: 70 additions & 0 deletions packages/dynamic-links/e2e/dynamicLinks.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]];
Expand Down
34 changes: 30 additions & 4 deletions packages/dynamic-links/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,18 +403,22 @@ 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
*
* 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). 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;
}
Expand Down Expand Up @@ -558,6 +562,28 @@ export namespace FirebaseDynamicLinksTypes {
* @param listener The listener callback, called with Dynamic Link instances.
*/
onLink(listener: Function<DynamicLink>): 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<DynamicLink>;
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/dynamic-links/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
51 changes: 51 additions & 0 deletions packages/dynamic-links/lib/index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -534,6 +564,27 @@ export interface Module extends ReactNativeFirebaseModule {
* @param listener The listener callback, called URL open events.
*/
onLink(listener: Function<string>): 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<string>;
}

declare module '@react-native-firebase/dynamic-links' {
Expand Down

0 comments on commit c43e8f7

Please sign in to comment.