Skip to content

Commit

Permalink
[cloud_functions] web plugin implementation (firebase#1890)
Browse files Browse the repository at this point in the history
* Initial release of cloud_functions_web
  • Loading branch information
sbeitzel authored Jan 29, 2020
1 parent 86c1024 commit a0b51b5
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ build/
.project
.classpath
.settings
.flutter-plugins-dependencies

1 change: 1 addition & 0 deletions packages/cloud_functions/cloud_functions_web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.flutter-plugins-dependencies
3 changes: 3 additions & 0 deletions packages/cloud_functions/cloud_functions_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 1.0.0

Initial release
27 changes: 27 additions & 0 deletions packages/cloud_functions/cloud_functions_web/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
84 changes: 84 additions & 0 deletions packages/cloud_functions/cloud_functions_web/README.md
Original file line number Diff line number Diff line change
@@ -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
<html>
...
<body>
<script src="https://www.gstatic.com/firebasejs/7.6.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.6.2/firebase-functions.js"></script>
<!-- Other firebase SDKs/config here -->
<script src="main.dart.js"></script>
</body>
</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
<body>
<!-- Previously loaded Firebase SDKs -->

<!-- ADD THIS BEFORE YOUR main.dart.js SCRIPT -->
<script>
// TODO: Replace the following with your app's Firebase project configuration.
// See: https://support.google.com/firebase/answer/7015592
var firebaseConfig = {
apiKey: "...",
authDomain: "[YOUR_PROJECT].firebaseapp.com",
databaseURL: "https://[YOUR_PROJECT].firebaseio.com",
projectId: "[YOUR_PROJECT]",
storageBucket: "[YOUR_PROJECT].appspot.com",
messagingSenderId: "...",
appId: "1:...:web:...",
measurementId: "G-..."
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
</script>
<!-- END OF FIREBASE INIT CODE -->

<script src="main.dart.js"></script>
</body>
```

### 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
Original file line number Diff line number Diff line change
@@ -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<dynamic> 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;
});
}
}
34 changes: 34 additions & 0 deletions packages/cloud_functions/cloud_functions_web/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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"

Original file line number Diff line number Diff line change
@@ -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<Map<String, dynamic>> log = <Map<String, dynamic>>[];

Map<String, dynamic> loggingCall(
{@required String appName,
@required String functionName,
String region,
dynamic parameters}) {
log.add(<String, dynamic>{
'appName': appName,
'functionName': functionName,
'region': region
});
return <String, dynamic>{
'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<String, dynamic> 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<String, dynamic> 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, <Matcher>[
equals(<String, dynamic>{
'appName': '[DEFAULT]',
'functionName': 'foobie',
'region': null
}),
]);
});

test('callCloudFunction calls down to Firebase API', () async {
log.clear();

CloudFunctionsPlatform cfp = CloudFunctionsPlatform.instance;
expect(cfp, isA<CloudFunctionsWeb>());

await cfp.callCloudFunction(
appName: '[DEFAULT]', functionName: 'baz', region: 'space');
await cfp.callCloudFunction(appName: 'mock', functionName: 'mumble');

expect(
log,
<Matcher>[
equals(<String, dynamic>{
'appName': '[DEFAULT]',
'functionName': 'baz',
'region': 'space'
}),
equals(<String, dynamic>{
'appName': 'mock',
'functionName': 'mumble',
'region': null
}),
],
);
});
});
}

Promise _jsPromise(dynamic value) {
return Promise(allowInterop((void resolve(dynamic result), Function reject) {
resolve(value);
}));
}
Loading

0 comments on commit a0b51b5

Please sign in to comment.