Skip to content

Commit 357ba72

Browse files
committed
feat(functions): implement getCallableFromUrl(url)
Fixes #6622 Related #6543
1 parent 81040b1 commit 357ba72

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

packages/functions/android/src/main/java/io/invertase/firebase/functions/UniversalFirebaseFunctionsModule.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.google.firebase.functions.FirebaseFunctions;
2626
import com.google.firebase.functions.HttpsCallableReference;
2727
import io.invertase.firebase.common.UniversalFirebaseModule;
28+
import java.net.URL;
2829
import java.util.concurrent.TimeUnit;
2930

3031
@SuppressWarnings("WeakerAccess")
@@ -65,4 +66,33 @@ Task<Object> httpsCallable(
6566
return Tasks.await(httpReference.call(data)).getData();
6667
});
6768
}
69+
70+
Task<Object> httpsCallableFromUrl(
71+
String appName,
72+
String region,
73+
String host,
74+
Integer port,
75+
String url,
76+
Object data,
77+
ReadableMap options) {
78+
return Tasks.call(
79+
getExecutor(),
80+
() -> {
81+
FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);
82+
FirebaseFunctions functionsInstance = FirebaseFunctions.getInstance(firebaseApp, region);
83+
URL parsedUrl = new URL(url);
84+
HttpsCallableReference httpReference =
85+
functionsInstance.getHttpsCallableFromUrl(parsedUrl);
86+
87+
if (options.hasKey("timeout")) {
88+
httpReference.setTimeout((long) options.getInt("timeout"), TimeUnit.SECONDS);
89+
}
90+
91+
if (host != null) {
92+
functionsInstance.useEmulator(host, port);
93+
}
94+
95+
return Tasks.await(httpReference.call(data)).getData();
96+
});
97+
}
6898
}

packages/functions/android/src/reactnative/java/io/invertase/firebase/functions/ReactNativeFirebaseFunctionsModule.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,55 @@ public void httpsCallable(
9393
promise.reject(code, message, exception, userInfo);
9494
});
9595
}
96+
97+
@ReactMethod
98+
public void httpsCallableFromUrl(
99+
String appName,
100+
String region,
101+
String host,
102+
Integer port,
103+
String url,
104+
ReadableMap wrapper,
105+
ReadableMap options,
106+
Promise promise) {
107+
Task<Object> callMethodTask =
108+
module.httpsCallableFromUrl(
109+
appName, region, host, port, url, wrapper.toHashMap().get(DATA_KEY), options);
110+
111+
// resolve
112+
callMethodTask.addOnSuccessListener(
113+
getExecutor(),
114+
result -> {
115+
promise.resolve(RCTConvertFirebase.mapPutValue(DATA_KEY, result, Arguments.createMap()));
116+
});
117+
118+
// reject
119+
callMethodTask.addOnFailureListener(
120+
getExecutor(),
121+
exception -> {
122+
Object details = null;
123+
String code = "UNKNOWN";
124+
String message = exception.getMessage();
125+
WritableMap userInfo = Arguments.createMap();
126+
if (exception.getCause() instanceof FirebaseFunctionsException) {
127+
FirebaseFunctionsException functionsException =
128+
(FirebaseFunctionsException) exception.getCause();
129+
details = functionsException.getDetails();
130+
code = functionsException.getCode().name();
131+
message = functionsException.getMessage();
132+
String timeout = FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name();
133+
Boolean isTimeout = code.contains(timeout);
134+
135+
if (functionsException.getCause() instanceof IOException && !isTimeout) {
136+
// return UNAVAILABLE for network io errors, to match iOS
137+
code = FirebaseFunctionsException.Code.UNAVAILABLE.name();
138+
message = FirebaseFunctionsException.Code.UNAVAILABLE.name();
139+
}
140+
}
141+
RCTConvertFirebase.mapPutValue(CODE_KEY, code, userInfo);
142+
RCTConvertFirebase.mapPutValue(MSG_KEY, message, userInfo);
143+
RCTConvertFirebase.mapPutValue(DETAILS_KEY, details, userInfo);
144+
promise.reject(code, message, exception, userInfo);
145+
});
146+
}
96147
}

packages/functions/e2e/functions.e2e.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ describe('functions()', function () {
100100
});
101101
});
102102

103+
describe('httpsCallableFromUrl()', function () {
104+
it('Calls a function by URL', async function () {
105+
let hostname = 'localhost';
106+
if (device.getPlatform() === 'android') {
107+
hostname = '10.0.2.2';
108+
}
109+
const functionRunner = firebase
110+
.functions()
111+
.httpsCallableFromUrl(
112+
`http://${hostname}:5001/react-native-firebase-testing/us-central1/helloWorld`,
113+
);
114+
const response = await functionRunner();
115+
response.data.should.equal('Hello from Firebase!');
116+
});
117+
});
118+
103119
describe('httpsCallable(fnName)(args)', function () {
104120
it('accepts primitive args: undefined', async function () {
105121
const functionRunner = firebase.functions().httpsCallable('testFunctionDefaultRegion');

packages/functions/ios/RNFBFunctions/RNFBFunctionsModule.m

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,58 @@ @implementation RNFBFunctionsModule
8080
}];
8181
}
8282

83+
RCT_EXPORT_METHOD(httpsCallableFromUrl
84+
: (FIRApp *)firebaseApp customUrlOrRegion
85+
: (NSString *)customUrlOrRegion host
86+
: (NSString *)host port
87+
: (NSNumber *_Nonnull)port url
88+
: (NSString *)url wrapper
89+
: (NSDictionary *)wrapper options
90+
: (NSDictionary *)options resolver
91+
: (RCTPromiseResolveBlock)resolve rejecter
92+
: (RCTPromiseRejectBlock)reject) {
93+
NSURL *customUrl = [NSURL URLWithString:customUrlOrRegion];
94+
FIRFunctions *functions =
95+
(customUrl && customUrl.scheme && customUrl.host)
96+
? [FIRFunctions functionsForApp:firebaseApp customDomain:customUrlOrRegion]
97+
: [FIRFunctions functionsForApp:firebaseApp region:customUrlOrRegion];
98+
99+
if (host != nil) {
100+
[functions useEmulatorWithHost:host port:[port intValue]];
101+
}
102+
103+
NSURL *functionUrl = [NSURL URLWithString:url];
104+
105+
FIRHTTPSCallable *callable = [functions HTTPSCallableWithURL:functionUrl];
106+
107+
if (options[@"timeout"]) {
108+
callable.timeoutInterval = [options[@"timeout"] doubleValue];
109+
}
110+
111+
[callable callWithObject:[wrapper valueForKey:@"data"]
112+
completion:^(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error) {
113+
if (error) {
114+
NSObject *details = [NSNull null];
115+
NSString *message = error.localizedDescription;
116+
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
117+
if ([error.domain isEqual:@"com.firebase.functions"]) {
118+
details = error.userInfo[@"details"];
119+
if (details == nil) {
120+
details = [NSNull null];
121+
}
122+
}
123+
124+
userInfo[@"code"] = [self getErrorCodeName:error];
125+
userInfo[@"message"] = message;
126+
userInfo[@"details"] = details;
127+
128+
[RNFBSharedUtils rejectPromiseWithUserInfo:reject userInfo:userInfo];
129+
} else {
130+
resolve(@{@"data" : [result data]});
131+
}
132+
}];
133+
}
134+
83135
- (NSString *)getErrorCodeName:(NSError *)error {
84136
NSString *code = @"UNKNOWN";
85137
switch (error.code) {

packages/functions/lib/index.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,29 @@ export namespace FirebaseFunctionsTypes {
338338
*/
339339
httpsCallable(name: string, options?: HttpsCallableOptions): HttpsCallable;
340340

341+
/**
342+
* Gets an `HttpsCallable` instance that refers to the function with the given
343+
* URL.
344+
*
345+
* #### Example
346+
*
347+
* ```js
348+
* const instance = firebase.functions().httpsCallable('order');
349+
*
350+
* try {
351+
* const response = await instance({
352+
* id: '12345',
353+
* });
354+
* } catch (e) {
355+
* console.error(e);
356+
* }
357+
* ```
358+
*
359+
* @param name The name of the https callable function.
360+
* @return The `HttpsCallable` instance.
361+
*/
362+
httpsCallableFromUrl(url: string, options?: HttpsCallableOptions): HttpsCallable;
363+
341364
/**
342365
* Changes this instance to point to a Cloud Functions emulator running locally.
343366
*

packages/functions/lib/index.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,39 @@ class FirebaseFunctionsModule extends FirebaseModule {
9393
};
9494
}
9595

96+
httpsCallableFromUrl(url, options = {}) {
97+
if (options.timeout) {
98+
if (isNumber(options.timeout)) {
99+
options.timeout = options.timeout / 1000;
100+
} else {
101+
throw new Error('HttpsCallableOptions.timeout expected a Number in milliseconds');
102+
}
103+
}
104+
105+
return data => {
106+
const nativePromise = this.native.httpsCallableFromUrl(
107+
this._useFunctionsEmulatorHost,
108+
this._useFunctionsEmulatorPort,
109+
url,
110+
{
111+
data,
112+
},
113+
options,
114+
);
115+
return nativePromise.catch(nativeError => {
116+
const { code, message, details } = nativeError.userInfo || {};
117+
return Promise.reject(
118+
new HttpsError(
119+
HttpsErrorCode[code] || HttpsErrorCode.UNKNOWN,
120+
message || nativeError.message,
121+
details || null,
122+
nativeError,
123+
),
124+
);
125+
});
126+
};
127+
}
128+
96129
useFunctionsEmulator(origin) {
97130
[_, host, port] = /https?\:.*\/\/([^:]+):?(\d+)?/.exec(origin);
98131
if (!port) {

0 commit comments

Comments
 (0)