Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): add support for 2nd gen functions #10545

Merged
merged 15 commits into from Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
359 changes: 167 additions & 192 deletions .github/workflows/scripts/functions/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions .github/workflows/scripts/functions/package.json
Expand Up @@ -14,8 +14,8 @@
},
"main": "lib/index.js",
"dependencies": {
"firebase-admin": "^11.4.1",
"firebase-functions": "^3.24.1"
"firebase-admin": "^11.5.0",
"firebase-functions": "^4.2.1"
},
"devDependencies": {
"firebase-functions-test": "^0.2.0",
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/scripts/functions/src/index.ts
@@ -1,12 +1,17 @@
import * as assert from 'assert';
import * as functions from 'firebase-functions';
import * as functionsv2 from 'firebase-functions/v2';

// For example app.
// noinspection JSUnusedGlobalSymbols
export const listFruit = functions.https.onCall(() => {
return ['Apple', 'Banana', 'Cherry', 'Date', 'Fig', 'Grapes'];
});

export const listfruits2ndgen = functionsv2.https.onCall(() => {
return ['Apple', 'Banana', 'Cherry', 'Date', 'Fig', 'Grapes'];
});

// For e2e testing a custom region.
// noinspection JSUnusedGlobalSymbols
export const testFunctionCustomRegion = functions
Expand Down
Expand Up @@ -23,6 +23,7 @@
import io.flutter.plugins.firebase.core.FlutterFirebasePlugin;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -69,7 +70,8 @@ private Task<Object> httpsFunctionCall(Map<String, Object> arguments) {

FirebaseFunctions firebaseFunctions = getFunctions(arguments);

String functionName = (String) Objects.requireNonNull(arguments.get("functionName"));
String functionName = (String) arguments.get("functionName");
String functionUri = (String) arguments.get("functionUri");
String origin = (String) arguments.get("origin");
Integer timeout = (Integer) arguments.get("timeout");
Object parameters = arguments.get("parameters");
Expand All @@ -79,8 +81,16 @@ private Task<Object> httpsFunctionCall(Map<String, Object> arguments) {
firebaseFunctions.useEmulator(originUri.getHost(), originUri.getPort());
}

HttpsCallableReference httpsCallableReference =
firebaseFunctions.getHttpsCallable(functionName);
HttpsCallableReference httpsCallableReference;

if (functionName != null) {
httpsCallableReference = firebaseFunctions.getHttpsCallable(functionName);
} else if (functionUri != null) {
httpsCallableReference =
firebaseFunctions.getHttpsCallableFromUrl(new URL(functionUri));
} else {
throw new IllegalArgumentException("Either functionName or functionUri must be set");
}

if (timeout != null) {
httpsCallableReference.setTimeout(timeout.longValue(), TimeUnit.MILLISECONDS);
Expand Down
Expand Up @@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion 29
compileSdkVersion 31

lintOptions {
disable 'InvalidPackage'
Expand Down
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -216,6 +216,7 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down Expand Up @@ -268,6 +269,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
Expand Up @@ -48,5 +48,7 @@
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
98 changes: 69 additions & 29 deletions packages/cloud_functions/cloud_functions/example/lib/main.dart
Expand Up @@ -3,10 +3,12 @@
// found in the LICENSE file.

import 'dart:core';
import 'dart:io';

import 'package:cloud_functions/cloud_functions.dart';
import 'package:cloud_functions_example/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
Expand Down Expand Up @@ -34,6 +36,9 @@ class _MyAppState extends State<MyApp> {

@override
Widget build(BuildContext context) {
final localhostMapped =
kIsWeb || !Platform.isAndroid ? 'localhost' : '10.0.2.2';

return MaterialApp(
home: Scaffold(
appBar: AppBar(
Expand All @@ -50,39 +55,74 @@ class _MyAppState extends State<MyApp> {
),
),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton.extended(
onPressed: () async {
// See index.js in .github/workflows/scripts for the example function we
// are using for this example
HttpsCallable callable = FirebaseFunctions.instance.httpsCallable(
'listFruit',
options: HttpsCallableOptions(
timeout: const Duration(seconds: 5),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: () async {
// See .github/workflows/scripts/functions/src/index.ts for the example function we
// are using for this example
HttpsCallable callable =
FirebaseFunctions.instance.httpsCallable(
'listFruit',
options: HttpsCallableOptions(
timeout: const Duration(seconds: 5),
),
);

await callingFunction(callable, context);
},
label: const Text('Call Function'),
icon: const Icon(Icons.cloud),
backgroundColor: Colors.deepOrange,
),
);
const SizedBox(height: 10),
FloatingActionButton.extended(
onPressed: () async {
// See .github/workflows/scripts/functions/src/index.ts for the example function we
// are using for this example
HttpsCallable callable =
FirebaseFunctions.instance.httpsCallable(
'http://$localhostMapped:5001/flutterfire-e2e-tests/us-central1/listfruits2ndgen',
options: HttpsCallableOptions(
timeout: const Duration(seconds: 5),
),
);

try {
final result = await callable();
setState(() {
fruit.clear();
result.data.forEach((f) {
fruit.add(f);
});
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('ERROR: $e'),
),
);
}
},
label: const Text('Call Function'),
icon: const Icon(Icons.cloud),
backgroundColor: Colors.deepOrange,
),
await callingFunction(callable, context);
},
label: const Text('Call 2nd Gen Function'),
icon: const Icon(Icons.cloud),
backgroundColor: Colors.deepOrange,
),
],
);
},
),
),
);
}

Future<void> callingFunction(
HttpsCallable callable,
BuildContext context,
) async {
try {
final result = await callable();
setState(() {
fruit.clear();
result.data.forEach((f) {
fruit.add(f);
});
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('ERROR: $e'),
),
);
}
}
}
Expand Up @@ -81,6 +81,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
- (void)httpsFunctionCall:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
NSString *appName = arguments[@"appName"];
NSString *functionName = arguments[@"functionName"];
NSString *functionUri = arguments[@"functionUri"];
NSString *origin = arguments[@"origin"];
NSString *region = arguments[@"region"];
NSNumber *timeout = arguments[@"timeout"];
Expand All @@ -93,7 +94,17 @@ - (void)httpsFunctionCall:(id)arguments withMethodCallResult:(FLTFirebaseMethodC
[functions useEmulatorWithHost:[url host] port:[[url port] intValue]];
}

FIRHTTPSCallable *function = [functions HTTPSCallableWithName:functionName];
FIRHTTPSCallable *function;

if (![functionName isEqual:[NSNull null]]) {
function = [functions HTTPSCallableWithName:functionName];
} else if (![functionUri isEqual:[NSNull null]]) {
function = [functions HTTPSCallableWithURL:[NSURL URLWithString:functionUri]];
} else {
result.error(@"IllegalArgumentException", @"Either functionName or functionUri must be set",
russellwheatley marked this conversation as resolved.
Show resolved Hide resolved
nil, nil);
return;
}
if (timeout != nil && ![timeout isEqual:[NSNull null]]) {
function.timeoutInterval = timeout.doubleValue / 1000;
}
Expand Down
Expand Up @@ -60,10 +60,47 @@ class FirebaseFunctions extends FirebasePluginPlatform {
String? _origin;

/// A reference to the Callable HTTPS trigger with the given name.
HttpsCallable httpsCallable(String name, {HttpsCallableOptions? options}) {
assert(name.isNotEmpty);
///
/// Should be the name of the Callable function in Firebase
/// or the URL of the 2nd gen Callable function in Firebase.
Lyokone marked this conversation as resolved.
Show resolved Hide resolved
HttpsCallable httpsCallable(
String nameOrUri, {
HttpsCallableOptions? options,
}) {
assert(nameOrUri.isNotEmpty);
final uri = Uri.tryParse(nameOrUri);
options ??= HttpsCallableOptions();
if (uri == null || uri.scheme.isEmpty) {
return HttpsCallable._(
delegate.httpsCallable(_origin, nameOrUri, options));
}
return HttpsCallable._(
delegate.httpsCallableWithUri(_origin, uri, options));
}

/// A reference to the Callable HTTPS trigger with the given URL.
///
/// Should be URL of the 2nd gen Callable function in Firebase.
HttpsCallable httpsCallableFromUrl(
String url, {
HttpsCallableOptions? options,
}) {
final uri = Uri.parse(url);
options ??= HttpsCallableOptions();
return HttpsCallable._(
delegate.httpsCallableWithUri(_origin, uri, options));
}

/// A reference to the Callable HTTPS trigger with the given Uri.
///
/// Should be Uri of the 2nd gen Callable function in Firebase.
HttpsCallable httpsCallableFromUri(
Uri uri, {
HttpsCallableOptions? options,
}) {
options ??= HttpsCallableOptions();
return HttpsCallable._(delegate.httpsCallable(_origin, name, options));
return HttpsCallable._(
delegate.httpsCallableWithUri(_origin, uri, options));
}

/// Changes this instance to point to a Cloud Functions emulator running locally.
Expand Down
14 changes: 11 additions & 3 deletions packages/cloud_functions/cloud_functions/test/mock.dart
Expand Up @@ -29,8 +29,8 @@ void resetFirebaseCoreMocks() {

class MockHttpsCallablePlatform extends HttpsCallablePlatform {
MockHttpsCallablePlatform(FirebaseFunctionsPlatform functions, String? origin,
String name, HttpsCallableOptions options)
: super(functions, origin, name, options);
String? name, HttpsCallableOptions options, Uri? uri)
: super(functions, origin, name, options, uri);

@override
Future<dynamic> call([dynamic parameters]) async {
Expand All @@ -47,7 +47,15 @@ class MockFirebaseFunctionsPlatform extends FirebaseFunctionsPlatform {
HttpsCallablePlatform httpsCallable(
String? origin, String name, HttpsCallableOptions options) {
HttpsCallablePlatform httpsCallablePlatform =
MockHttpsCallablePlatform(this, origin, name, options);
MockHttpsCallablePlatform(this, origin, name, options, null);
return httpsCallablePlatform;
}

@override
HttpsCallablePlatform httpsCallableWithUri(
String? origin, Uri uri, HttpsCallableOptions options) {
HttpsCallablePlatform httpsCallablePlatform =
MockHttpsCallablePlatform(this, origin, null, options, uri);
return httpsCallablePlatform;
}

Expand Down
Expand Up @@ -42,6 +42,12 @@ class MethodChannelFirebaseFunctions extends FirebaseFunctionsPlatform {
@override
HttpsCallablePlatform httpsCallable(
String? origin, String name, HttpsCallableOptions options) {
return MethodChannelHttpsCallable(this, origin, name, options);
return MethodChannelHttpsCallable(this, origin, name, options, null);
}

@override
HttpsCallablePlatform httpsCallableWithUri(
String? origin, Uri uri, HttpsCallableOptions options) {
return MethodChannelHttpsCallable(this, origin, null, options, uri);
}
}
Expand Up @@ -7,15 +7,14 @@ import 'dart:async';

import '../../cloud_functions_platform_interface.dart';
import 'method_channel_firebase_functions.dart';

import 'utils/exception.dart';

/// Method Channel delegate for [HttpsCallablePlatform].
class MethodChannelHttpsCallable extends HttpsCallablePlatform {
/// Creates a new [MethodChannelHttpsCallable] instance.
MethodChannelHttpsCallable(FirebaseFunctionsPlatform functions,
String? origin, String name, HttpsCallableOptions options)
: super(functions, origin, name, options);
String? origin, String? name, HttpsCallableOptions options, Uri? uri)
: super(functions, origin, name, options, uri);

@override
Future<dynamic> call([Object? parameters]) async {
Expand All @@ -24,6 +23,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
.invokeMethod('FirebaseFunctions#call', <String, dynamic>{
'appName': functions.app!.name,
'functionName': name,
'functionUri': uri?.toString(),
'origin': origin,
'region': functions.region,
'timeout': options.timeout.inMilliseconds,
Expand Down
Expand Up @@ -66,4 +66,10 @@ abstract class FirebaseFunctionsPlatform extends PlatformInterface {
String? origin, String name, HttpsCallableOptions options) {
throw UnimplementedError('httpsCallable() is not implemented');
}

/// Creates a [HttpsCallablePlatform] instance from a [Uri]
HttpsCallablePlatform httpsCallableWithUri(
String? origin, Uri uri, HttpsCallableOptions options) {
throw UnimplementedError('httpsCallable() is not implemented');
}
}