From a0b51b59d60a1b08b0e7d886cd6977740603f8fe Mon Sep 17 00:00:00 2001 From: Stephen Beitzel Date: Wed, 29 Jan 2020 07:02:36 -0800 Subject: [PATCH] [cloud_functions] web plugin implementation (#1890) * Initial release of cloud_functions_web --- .cirrus.yml | 2 +- .gitignore | 2 + .../cloud_functions_web/.gitignore | 1 + .../cloud_functions_web/CHANGELOG.md | 3 + .../cloud_functions_web/LICENSE | 27 ++++ .../cloud_functions_web/README.md | 84 +++++++++++ .../lib/cloud_functions_web.dart | 63 ++++++++ .../cloud_functions_web/pubspec.yaml | 34 +++++ .../test/cloud_functions_web_test.dart | 134 ++++++++++++++++++ .../test/mock/firebase_mock.dart | 80 +++++++++++ 10 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 packages/cloud_functions/cloud_functions_web/.gitignore create mode 100644 packages/cloud_functions/cloud_functions_web/CHANGELOG.md create mode 100644 packages/cloud_functions/cloud_functions_web/LICENSE create mode 100644 packages/cloud_functions/cloud_functions_web/README.md create mode 100644 packages/cloud_functions/cloud_functions_web/lib/cloud_functions_web.dart create mode 100644 packages/cloud_functions/cloud_functions_web/pubspec.yaml create mode 100644 packages/cloud_functions/cloud_functions_web/test/cloud_functions_web_test.dart create mode 100644 packages/cloud_functions/cloud_functions_web/test/mock/firebase_mock.dart diff --git a/.cirrus.yml b/.cirrus.yml index 31bd4c58a8b0..0f73d8cf1f14 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -90,7 +90,7 @@ task: activate_script: pub global activate flutter_plugin_tools create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-2 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot matrix: - name: build-ipas+drive-examples env: diff --git a/.gitignore b/.gitignore index ccb0eeb34605..0f44f9ac1214 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ build/ .project .classpath .settings +.flutter-plugins-dependencies + diff --git a/packages/cloud_functions/cloud_functions_web/.gitignore b/packages/cloud_functions/cloud_functions_web/.gitignore new file mode 100644 index 000000000000..c5fa4f8189fa --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/.gitignore @@ -0,0 +1 @@ +.flutter-plugins-dependencies diff --git a/packages/cloud_functions/cloud_functions_web/CHANGELOG.md b/packages/cloud_functions/cloud_functions_web/CHANGELOG.md new file mode 100644 index 000000000000..9d241d49741c --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +Initial release diff --git a/packages/cloud_functions/cloud_functions_web/LICENSE b/packages/cloud_functions/cloud_functions_web/LICENSE new file mode 100644 index 000000000000..3b8d61a546d4 --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2017-2020 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/cloud_functions/cloud_functions_web/README.md b/packages/cloud_functions/cloud_functions_web/README.md new file mode 100644 index 000000000000..21cb36fb3426 --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/README.md @@ -0,0 +1,84 @@ +# cloud_functions_web + +The web implementation of [`cloud_functions`][1]. + +## Usage + +### Import the package + +**TODO(sbeitzel) - update the versions here so that it's correct, once this package actually _is_ an endorsed implementation of `package:cloud_functions`** + +This package is the endorsed implementation of `cloud_functions` for the web platform since version `0.0.1`, so it gets automatically added to your dependencies by depending on `cloud_functions: ^0.0.1`. + +No modifications to your `pubspec.yaml` should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): + +```yaml +... +dependencies: + ... + cloud_functions: ^0.0.1 + ... +``` + +### Updating `index.html` + +Due to [this bug in dartdevc][2], you will need to manually add the Firebase JavaScript file to your `index.html` file. + +In your app directory, edit `web/index.html` to add the line: + +```html + + ... + + + + + + + +``` + +### Initialize Firebase + +If your app is using the "default" Firebase app _(this means that you're not doing any `package:firebase_core` initialization yourself)_, you need to initialize it now, following the steps in the [Firebase Web Setup][3] docs. + +Specifically, you'll want to add the following lines to your `web/index.html` file: + +```html + + + + + + + + + +``` + +### Using the plugin + +Once you have modified your `web/index.html` file you should be able to use `package:cloud_functions` as normal. + +#### Examples + +* The `example` app in `package:cloud_functions` has an implementation of this instructions. + +[1]: ../cloud_functions +[2]: https://github.com/dart-lang/sdk/issues/33979 +[3]: https://firebase.google.com/docs/web/setup#add-sdks-initialize diff --git a/packages/cloud_functions/cloud_functions_web/lib/cloud_functions_web.dart b/packages/cloud_functions/cloud_functions_web/lib/cloud_functions_web.dart new file mode 100644 index 000000000000..ad28f75fabff --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/lib/cloud_functions_web.dart @@ -0,0 +1,63 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:cloud_functions_platform_interface/cloud_functions_platform_interface.dart'; +import 'package:firebase/firebase.dart' as firebase; +import 'package:flutter/foundation.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +/// Web implementation of [CloudFunctionsPlatform]. +class CloudFunctionsWeb extends CloudFunctionsPlatform { + /// Create the default instance of the [CloudFunctionsPlatform] as a [CloudFunctionsWeb] + static void registerWith(Registrar registrar) { + CloudFunctionsPlatform.instance = CloudFunctionsWeb(); + } + + /// Invokes the specified cloud function. + /// + /// The required parameters, [appName] and [functionName], specify which + /// cloud function will be called. + /// + /// The rest of the parameters are optional and used to invoke the function + /// with something other than the defaults. [region] defaults to `us-central1` + /// and [timeout] defaults to 60 seconds. + /// + /// The [origin] parameter may be used to provide the base URL for the function. + /// This can be used to send requests to a local emulator. + /// + /// The data passed into the cloud function via [parameters] can be any of the following types: + /// + /// `null` + /// `String` + /// `num` + /// [List], where the contained objects are also one of these types. + /// [Map], where the values are also one of these types. + @override + Future callCloudFunction({ + @required String appName, + @required String functionName, + String region, + String origin, + Duration timeout, + dynamic parameters, + }) { + firebase.App app = firebase.app(appName); + firebase.Functions functions = app.functions(region); + if (origin != null) { + functions.useFunctionsEmulator(origin); + } + firebase.HttpsCallable hc; + if (timeout != null) { + hc = functions.httpsCallable(functionName, + firebase.HttpsCallableOptions(timeout: timeout.inMicroseconds)); + } else { + hc = functions.httpsCallable(functionName); + } + return hc.call(parameters).then((result) { + return result.data; + }); + } +} diff --git a/packages/cloud_functions/cloud_functions_web/pubspec.yaml b/packages/cloud_functions/cloud_functions_web/pubspec.yaml new file mode 100644 index 000000000000..724bef271919 --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/pubspec.yaml @@ -0,0 +1,34 @@ +name: cloud_functions_web +description: The web implementation of cloud_functions +homepage: https://github.com/FirebaseExtended/flutterfire/tree/master/packages/cloud_functions/cloud_functions_web +version: 1.0.0 + +flutter: + plugin: + platforms: + web: + pluginClass: CloudFunctionsWeb + fileName: cloud_functions_web.dart + +dependencies: + cloud_functions_platform_interface: ^1.0.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + firebase: ^7.0.0 + http_parser: ^3.1.3 + meta: ^1.1.7 + +dev_dependencies: + flutter_test: + sdk: flutter + firebase_core_platform_interface: ^1.0.2 + firebase_core_web: ^0.1.1+2 + mockito: ^4.1.1 + js: ^0.6.0 + +environment: + sdk: ">=2.0.0-dev.28.0 <3.0.0" + flutter: ">=1.12.13+hotfix.4 <2.0.0" + diff --git a/packages/cloud_functions/cloud_functions_web/test/cloud_functions_web_test.dart b/packages/cloud_functions/cloud_functions_web/test/cloud_functions_web_test.dart new file mode 100644 index 000000000000..ff3554b78111 --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/test/cloud_functions_web_test.dart @@ -0,0 +1,134 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +@TestOn('chrome') + +import 'dart:js' show allowInterop; + +import 'package:cloud_functions_platform_interface/cloud_functions_platform_interface.dart'; +import 'package:cloud_functions_web/cloud_functions_web.dart'; +import 'package:firebase/firebase.dart' as firebase; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:firebase_core_web/firebase_core_web.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meta/meta.dart'; + +import 'mock/firebase_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$CloudFunctionsWeb', () { + final List> log = >[]; + + Map loggingCall( + {@required String appName, + @required String functionName, + String region, + dynamic parameters}) { + log.add({ + 'appName': appName, + 'functionName': functionName, + 'region': region + }); + return { + 'foo': 'bar', + }; + } + + setUp(() async { + firebaseMock = FirebaseMock( + app: allowInterop( + (String name) => FirebaseAppMock( + name: name, + options: FirebaseAppOptionsMock(appId: '123'), + functions: allowInterop(([region]) => FirebaseFunctionsMock( + httpsCallable: allowInterop((functionName, [options]) { + final String appName = name == null ? '[DEFAULT]' : name; + return allowInterop(([data]) { + Map result = loggingCall( + appName: appName, + functionName: functionName, + region: region); + return _jsPromise(FirebaseHttpsCallableResultMock( + data: allowInterop((_) => result))); + }); + }), + useFunctionsEmulator: allowInterop((url) { + print('Unimplemented. Supposed to emulate at $url'); + }), + ))), + )); + + FirebaseCorePlatform.instance = FirebaseCoreWeb(); + CloudFunctionsPlatform.instance = CloudFunctionsWeb(); + + // install loggingCall on the HttpsCallable mock as the thing that gets + // executed when its call method is invoked + firebaseMock.functions = allowInterop(([app]) => FirebaseFunctionsMock( + httpsCallable: allowInterop((functionName, [options]) { + final String appName = app == null ? '[DEFAULT]' : app.name; + return allowInterop(([data]) { + Map result = + loggingCall(appName: appName, functionName: functionName); + return _jsPromise(FirebaseHttpsCallableResultMock( + data: allowInterop((_) => result))); + }); + }), + useFunctionsEmulator: allowInterop((url) { + print('Unimplemented. Supposed to emulate at $url'); + }), + )); + }); + + test('setUp wires up mock objects properly', () async { + log.clear(); + + firebase.App app = firebase.app('[DEFAULT]'); + expect(app.options.appId, equals('123')); + firebase.Functions fs = firebase.functions(app); + firebase.HttpsCallable callable = fs.httpsCallable('foobie'); + await callable.call(); + expect(log, [ + equals({ + 'appName': '[DEFAULT]', + 'functionName': 'foobie', + 'region': null + }), + ]); + }); + + test('callCloudFunction calls down to Firebase API', () async { + log.clear(); + + CloudFunctionsPlatform cfp = CloudFunctionsPlatform.instance; + expect(cfp, isA()); + + await cfp.callCloudFunction( + appName: '[DEFAULT]', functionName: 'baz', region: 'space'); + await cfp.callCloudFunction(appName: 'mock', functionName: 'mumble'); + + expect( + log, + [ + equals({ + 'appName': '[DEFAULT]', + 'functionName': 'baz', + 'region': 'space' + }), + equals({ + 'appName': 'mock', + 'functionName': 'mumble', + 'region': null + }), + ], + ); + }); + }); +} + +Promise _jsPromise(dynamic value) { + return Promise(allowInterop((void resolve(dynamic result), Function reject) { + resolve(value); + })); +} diff --git a/packages/cloud_functions/cloud_functions_web/test/mock/firebase_mock.dart b/packages/cloud_functions/cloud_functions_web/test/mock/firebase_mock.dart new file mode 100644 index 000000000000..6ddf2903c97f --- /dev/null +++ b/packages/cloud_functions/cloud_functions_web/test/mock/firebase_mock.dart @@ -0,0 +1,80 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@JS() +library firebase_mock; + +import 'package:js/js.dart'; + +@JS() +@anonymous +class FirebaseAppOptionsMock { + external factory FirebaseAppOptionsMock({String appId}); + external String get appId; +} + +@JS() +@anonymous +class FirebaseAppMock { + external factory FirebaseAppMock({ + String name, + FirebaseAppOptionsMock options, + Function functions, + }); + external String get name; + external FirebaseAppOptionsMock get options; + external Function get functions; +} + +@JS() +@anonymous +class FirebaseFunctionsMock { + external factory FirebaseFunctionsMock({ + Function useFunctionsEmulator, + Function httpsCallable, + }); + + external Function get useFunctionsEmulator; + external Function get httpsCallable; +} + +@JS() +@anonymous +class FirebaseHttpsCallableMock { + external factory FirebaseHttpsCallableMock({ + Function call, + }); + + external Function get call; +} + +@JS() +@anonymous +class FirebaseHttpsCallableResultMock { + external factory FirebaseHttpsCallableResultMock({Function data}); + + external Function get data; +} + +@JS() +@anonymous +class FirebaseMock { + external factory FirebaseMock({Function app}); + external Function get app; + + external set functions(Function functions); + external Function get functions; +} + +@JS() +class Promise { + external Promise(void executor(void resolve(T result), Function reject)); + external Promise then(void onFulfilled(T result), [Function onRejected]); +} + +// Wire to the global 'window.firebase' object. +@JS('firebase') +external set firebaseMock(FirebaseMock mock); +@JS('firebase') +external FirebaseMock get firebaseMock;