Skip to content

Commit

Permalink
feat(functions): implement getCallableFromUrl(url)
Browse files Browse the repository at this point in the history
Fixes #6622
Related #6543
  • Loading branch information
mikehardy committed Oct 23, 2022
1 parent 81040b1 commit 357ba72
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.HttpsCallableReference;
import io.invertase.firebase.common.UniversalFirebaseModule;
import java.net.URL;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("WeakerAccess")
Expand Down Expand Up @@ -65,4 +66,33 @@ Task<Object> httpsCallable(
return Tasks.await(httpReference.call(data)).getData();
});
}

Task<Object> httpsCallableFromUrl(
String appName,
String region,
String host,
Integer port,
String url,
Object data,
ReadableMap options) {
return Tasks.call(
getExecutor(),
() -> {
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
FirebaseFunctions functionsInstance = FirebaseFunctions.getInstance(firebaseApp, region);
URL parsedUrl = new URL(url);
HttpsCallableReference httpReference =
functionsInstance.getHttpsCallableFromUrl(parsedUrl);

if (options.hasKey("timeout")) {
httpReference.setTimeout((long) options.getInt("timeout"), TimeUnit.SECONDS);
}

if (host != null) {
functionsInstance.useEmulator(host, port);
}

return Tasks.await(httpReference.call(data)).getData();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,55 @@ public void httpsCallable(
promise.reject(code, message, exception, userInfo);
});
}

@ReactMethod
public void httpsCallableFromUrl(
String appName,
String region,
String host,
Integer port,
String url,
ReadableMap wrapper,
ReadableMap options,
Promise promise) {
Task<Object> callMethodTask =
module.httpsCallableFromUrl(
appName, region, host, port, url, wrapper.toHashMap().get(DATA_KEY), options);

// resolve
callMethodTask.addOnSuccessListener(
getExecutor(),
result -> {
promise.resolve(RCTConvertFirebase.mapPutValue(DATA_KEY, result, Arguments.createMap()));
});

// reject
callMethodTask.addOnFailureListener(
getExecutor(),
exception -> {
Object details = null;
String code = "UNKNOWN";
String message = exception.getMessage();
WritableMap userInfo = Arguments.createMap();
if (exception.getCause() instanceof FirebaseFunctionsException) {
FirebaseFunctionsException functionsException =
(FirebaseFunctionsException) exception.getCause();
details = functionsException.getDetails();
code = functionsException.getCode().name();
message = functionsException.getMessage();
String timeout = FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name();
Boolean isTimeout = code.contains(timeout);

if (functionsException.getCause() instanceof IOException && !isTimeout) {
// return UNAVAILABLE for network io errors, to match iOS
code = FirebaseFunctionsException.Code.UNAVAILABLE.name();
message = FirebaseFunctionsException.Code.UNAVAILABLE.name();
}
}
RCTConvertFirebase.mapPutValue(CODE_KEY, code, userInfo);
RCTConvertFirebase.mapPutValue(MSG_KEY, message, userInfo);
RCTConvertFirebase.mapPutValue(DETAILS_KEY, details, userInfo);
promise.reject(code, message, exception, userInfo);
});
}
}
16 changes: 16 additions & 0 deletions packages/functions/e2e/functions.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,22 @@ describe('functions()', function () {
});
});

describe('httpsCallableFromUrl()', function () {
it('Calls a function by URL', async function () {
let hostname = 'localhost';
if (device.getPlatform() === 'android') {
hostname = '10.0.2.2';
}
const functionRunner = firebase
.functions()
.httpsCallableFromUrl(
`http://${hostname}:5001/react-native-firebase-testing/us-central1/helloWorld`,
);
const response = await functionRunner();
response.data.should.equal('Hello from Firebase!');
});
});

describe('httpsCallable(fnName)(args)', function () {
it('accepts primitive args: undefined', async function () {
const functionRunner = firebase.functions().httpsCallable('testFunctionDefaultRegion');
Expand Down
52 changes: 52 additions & 0 deletions packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,58 @@ @implementation RNFBFunctionsModule
}];
}

RCT_EXPORT_METHOD(httpsCallableFromUrl
: (FIRApp *)firebaseApp customUrlOrRegion
: (NSString *)customUrlOrRegion host
: (NSString *)host port
: (NSNumber *_Nonnull)port url
: (NSString *)url wrapper
: (NSDictionary *)wrapper options
: (NSDictionary *)options resolver
: (RCTPromiseResolveBlock)resolve rejecter
: (RCTPromiseRejectBlock)reject) {
NSURL *customUrl = [NSURL URLWithString:customUrlOrRegion];
FIRFunctions *functions =
(customUrl && customUrl.scheme && customUrl.host)
? [FIRFunctions functionsForApp:firebaseApp customDomain:customUrlOrRegion]
: [FIRFunctions functionsForApp:firebaseApp region:customUrlOrRegion];

if (host != nil) {
[functions useEmulatorWithHost:host port:[port intValue]];
}

NSURL *functionUrl = [NSURL URLWithString:url];

FIRHTTPSCallable *callable = [functions HTTPSCallableWithURL:functionUrl];

if (options[@"timeout"]) {
callable.timeoutInterval = [options[@"timeout"] doubleValue];
}

[callable callWithObject:[wrapper valueForKey:@"data"]
completion:^(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error) {
if (error) {
NSObject *details = [NSNull null];
NSString *message = error.localizedDescription;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
if ([error.domain isEqual:@"com.firebase.functions"]) {
details = error.userInfo[@"details"];
if (details == nil) {
details = [NSNull null];
}
}

userInfo[@"code"] = [self getErrorCodeName:error];
userInfo[@"message"] = message;
userInfo[@"details"] = details;

[RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:userInfo];
} else {
resolve(@{@"data" : [result data]});
}
}];
}

- (NSString *)getErrorCodeName:(NSError *)error {
NSString *code = @"UNKNOWN";
switch (error.code) {
Expand Down
23 changes: 23 additions & 0 deletions packages/functions/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,29 @@ export namespace FirebaseFunctionsTypes {
*/
httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable;

/**
* Gets an `HttpsCallable` instance that refers to the function with the given
* URL.
*
* #### Example
*
* ```js
* const instance = firebase.functions().httpsCallable('order');
*
* try {
* const response = await instance({
* id: '12345',
* });
* } catch (e) {
* console.error(e);
* }
* ```
*
* @param name The name of the https callable function.
* @return The `HttpsCallable` instance.
*/
httpsCallableFromUrl(url: string, options?: HttpsCallableOptions): HttpsCallable;

/**
* Changes this instance to point to a Cloud Functions emulator running locally.
*
Expand Down
33 changes: 33 additions & 0 deletions packages/functions/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ class FirebaseFunctionsModule extends FirebaseModule {
};
}

httpsCallableFromUrl(url, options = {}) {
if (options.timeout) {
if (isNumber(options.timeout)) {
options.timeout = options.timeout / 1000;
} else {
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
}
}

return data => {
const nativePromise = this.native.httpsCallableFromUrl(
this._useFunctionsEmulatorHost,
this._useFunctionsEmulatorPort,
url,
{
data,
},
options,
);
return nativePromise.catch(nativeError => {
const { code, message, details } = nativeError.userInfo || {};
return Promise.reject(
new HttpsError(
HttpsErrorCode[code] || HttpsErrorCode.UNKNOWN,
message || nativeError.message,
details || null,
nativeError,
),
);
});
};
}

useFunctionsEmulator(origin) {
[_, host, port] = /https?\:.*\/\/([^:]+):?(\d+)?/.exec(origin);
if (!port) {
Expand Down

5 comments on commit 357ba72

@vercel
Copy link

@vercel vercel bot commented on 357ba72 Oct 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trevor-rex
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status on this PR? Is this the single PR that is blocking being able to call oncall v2 function endpoints with react-native-firebase?

@mikehardy
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the status on this PR? Is this the single PR that is blocking being able to call oncall v2 function endpoints with react-native-firebase?

Hi there - you are commenting on a commit which includes a list of tags it has been included in. It was first released in v16.2.0 and I assume it will show up in the changelog associated with that release. So it's out already and has been for a while? Is it not working? I was using it personally to call v2 functions in an app, that's why I made it - along with community interest. I think it's working...

@trevor-rex
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the information! My team is coming back from a break since early December and I believe that is when we last tried it. We may not have been on the latest version so we will try again. The issue this PR was linked on is the only documentation I could find related to this so apologies.

@mikehardy
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I thought it was neat but also a bit niche so didn't really publicize it, just snuck the PR in and shipped it ;-). If it's not working please open an issue and let me know. Good luck

Please sign in to comment.