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(firebase_remote_config): Firebase Remote Config for Dart and Desktop #71

Draft
wants to merge 31 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
302720b
boilerplate remote config
TimWhiting Apr 24, 2022
e7fb658
start of dart
TimWhiting Apr 24, 2022
bff7c98
add in todos, fix static errors
TimWhiting Apr 24, 2022
dec2ddd
start on storage
TimWhiting Apr 24, 2022
e00a5d0
start on storage
TimWhiting Apr 24, 2022
de2506d
clean up some things
TimWhiting Apr 24, 2022
faa19a6
finish basic storage, move on to rest api
TimWhiting Apr 26, 2022
943ed17
finish dart package implementation
TimWhiting Apr 27, 2022
093c6e7
- move json serialization to dataclass
TimWhiting Apr 27, 2022
be121f0
start working on tests
TimWhiting Apr 27, 2022
76832c6
basic tests
TimWhiting Apr 28, 2022
dc87e69
start on example continue testing
TimWhiting Apr 28, 2022
11c44dd
get working with example app
TimWhiting May 14, 2022
5299781
remove firebaseapis
TimWhiting May 14, 2022
56401b6
remove reference to firebaseapis
TimWhiting May 14, 2022
ecdeec2
chore: upgrade `desktop_webview_auth` to v0.0.7
pr-Mais Apr 25, 2022
0d775b0
chore(release): publish packages
pr-Mais Apr 25, 2022
112e1f1
feat(firebase_auth_dart): web support (#72)
pr-Mais Apr 28, 2022
fe8d56b
feat(firebase_auth): Github auth provider (#74)
pr-Mais May 6, 2022
b71229a
chore(release): publish packages
pr-Mais May 6, 2022
e771a3f
refactor!: remove macOS support
pr-Mais May 9, 2022
9bbe72b
chore(release): publish packages
pr-Mais May 9, 2022
b45df7d
ci: remove macos tests job
pr-Mais May 9, 2022
1bfb131
fix(firebase_core): return existing app if options are matching if th…
pr-Mais May 11, 2022
6f6741c
chore(release): publish packages
pr-Mais May 11, 2022
f09fe11
firebaseapis
TimWhiting May 19, 2022
fb8efa7
some more attempts at fixing
TimWhiting May 19, 2022
3478130
fix using clientViaApiKey
TimWhiting May 20, 2022
5cb057c
Merge remote-tracking branch 'upstream/main' into remote_config
TimWhiting May 20, 2022
841ccdb
fix analyzer errors and pubspec
TimWhiting May 20, 2022
cf4c71b
start on fixing tests
TimWhiting May 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
.dart_tool/

.packages
.pub/

build/
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0-dev.0

- Initial release.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD-3-Clause
------------

Copyright (c) 2016-present Invertase Limited <[email protected]> & Contributors

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. 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.

3. Neither the name of the copyright holder 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 HOLDER 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.


Creative Commons Attribution 3.0 License
----------------------------------------

Copyright (c) 2016-present Invertase Limited <[email protected]> & Contributors

Documentation and other instructional materials provided for this project
(including on a separate documentation repository or it's documentation website) are
licensed under the Creative Commons Attribution 3.0 License. Code samples/blocks
contained therein are licensed under the BSD-3-Clause License (the "License"), as above.

You may obtain a copy of the Creative Commons Attribution 3.0 License at

https://creativecommons.org/licenses/by/3.0/
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
// Copyright 2021 Invertase Limited. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.

library firebase_remote_config_dart;

import 'dart:async';

import 'package:firebase_core_dart/firebase_core_dart.dart';
import 'package:firebaseapis/firebaseremoteconfig/v1.dart' as api;
import 'package:googleapis_auth/auth_io.dart';
import 'package:meta/meta.dart';
import 'package:storagebox/storagebox.dart';

import 'src/remote_config_settings.dart';
import 'src/remote_config_status.dart';
import 'src/remote_config_value.dart';

export 'src/remote_config_settings.dart';
export 'src/remote_config_status.dart';
export 'src/remote_config_value.dart';

part 'src/internal/api.dart';
part 'src/internal/storage.dart';

/// The entry point for accessing Remote Config.
///
/// You can get an instance by calling [FirebaseRemoteConfig.instance]. Note
/// [FirebaseRemoteConfig.instance] is async.
// TODO: The flutter implementation uses a ChangeNotifier to let someone listen should we use StateNotifier?
class FirebaseRemoteConfig {
/// Creates a new instance of FirebaseRemoteConfig
@visibleForTesting
FirebaseRemoteConfig({
required this.app,
this.namespace = 'firebase',
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what the namespace is used for? couldn't find it in the other plugins

}) : storage = _RemoteConfigStorage(app.options.appId, app.name, namespace);

// Cached instances of [FirebaseRemoteConfig].
static final Map<String, Map<String, FirebaseRemoteConfig>>
_firebaseRemoteConfigInstances = {};

/// The [FirebaseApp] this instance was initialized with.
final FirebaseApp app;

/// Returns an instance using the default [FirebaseApp].
static FirebaseRemoteConfig get instance {
return FirebaseRemoteConfig.instanceFor(app: Firebase.app());
}

/// Returns an instance using the specified [FirebaseApp].
// ignore: prefer_constructors_over_static_methods
static FirebaseRemoteConfig instanceFor({
FirebaseApp? app,
String namespace = 'firebase',
}) {
final _app = app ?? Firebase.app();
if (_firebaseRemoteConfigInstances[_app.name] == null) {
_firebaseRemoteConfigInstances[_app.name] = {};
}
return _firebaseRemoteConfigInstances[_app.name]!.putIfAbsent(
namespace,
() => FirebaseRemoteConfig(app: _app, namespace: namespace),
);
}

@visibleForTesting
// ignore: library_private_types_in_public_api, public_member_api_docs
late final _RemoteConfigStorageCache storageCache =
_RemoteConfigStorageCache(storage);
@visibleForTesting
// ignore: library_private_types_in_public_api, public_member_api_docs
final _RemoteConfigStorage storage;

/// The namespace of the remote config instance
final String namespace;

/// Returns the [DateTime] of the last successful fetch.
///
/// If no successful fetch has been made a [DateTime] representing
/// the epoch (1970-01-01 UTC) is returned.
DateTime get lastFetchTime =>
storageCache.lastFetchTime ?? DateTime.fromMicrosecondsSinceEpoch(0);

/// Returns the status of the last fetch attempt.
RemoteConfigFetchStatus get lastFetchStatus =>
storageCache.lastFetchStatus ?? RemoteConfigFetchStatus.noFetchYet;

/// Returns a copy of the [RemoteConfigSettings] of the current instance.
RemoteConfigSettings get settings => RemoteConfigSettings(
fetchTimeout: _settings.fetchTimeout,
minimumFetchInterval: _settings.minimumFetchInterval,
);
RemoteConfigSettings _settings = RemoteConfigSettings();

/// Default parameters set via [setDefaults]
Map<String, Object?> _defaultConfig = {};

/// Api
@visibleForTesting
late RemoteConfigApiClient api = RemoteConfigApiClient(
app.options.projectId,
namespace,
app.options.apiKey,
app.options.appId,
storage,
storageCache,
);

/// Makes the last fetched config available to getters.
///
/// Returns a [bool] that is true if the config parameters
/// were activated. Returns a [bool] that is false if the
/// config parameters were already activated.
Future<bool> activate() async {
final lastSuccessfulFetchResponse =
storage.getLastSuccessfulFetchResponse();

if (lastSuccessfulFetchResponse == null) {
return false;
} else {
final newConfig = <String, RemoteConfigValue>{
for (final entry in lastSuccessfulFetchResponse.entries)
entry.key: RemoteConfigValue(entry.value, ValueSource.valueRemote)
};
storageCache.setActiveConfig(newConfig);
return true;
}
}

final _initialized = Completer<void>();

/// Ensures the last activated config are available to getters.
Future<void> ensureInitialized() async {
// Somewhat unnecessary for desktop because we do synchronous file reads for storage
// Will be necessary if we ever support pure dart on web
if (!_initialized.isCompleted) {
await storageCache.loadFromStorage().then((_) {
_initialized.complete();
});
}
return _initialized.future;
}

/// Fetches and caches configuration from the Remote Config service.
Future<void> fetch() async {
try {
await api
.fetch(cacheMaxAge: settings.minimumFetchInterval)
.timeout(settings.fetchTimeout);
} on TimeoutException {
storageCache.setLastFetchStatus(RemoteConfigFetchStatus.throttle);
rethrow; // TODO: Throw Firebase Exception
} on Exception {
storageCache.setLastFetchStatus(RemoteConfigFetchStatus.failure);
rethrow; // TODO: Throw Firebase Exception
}
storageCache.setLastFetchStatus(RemoteConfigFetchStatus.success);
}

/// Performs a fetch and activate operation, as a convenience.
///
/// Returns [bool] in the same way that is done for [activate].
Future<bool> fetchAndActivate() async {
await fetch();
return activate();
}

/// Returns a Map of all Remote Config parameters.
Map<String, RemoteConfigValue> getAll() {
final allKeys = {
...?storageCache.activeConfig?.keys,
..._defaultConfig.keys
};
// Get the value for each key, respecting the default config
return {for (final key in allKeys) key: getValue(key)};
}

/// Gets the value for a given key as a bool.
bool getBool(String key) => getValue(key).asBool();

/// Gets the value for a given key as an int.
int getInt(String key) => getValue(key).asInt();

/// Gets the value for a given key as a double.
double getDouble(String key) => getValue(key).asDouble();

/// Gets the value for a given key as a String.
String getString(String key) => getValue(key).asString();

/// Gets the [RemoteConfigValue] for a given key.
RemoteConfigValue getValue(String key) {
assert(
_initialized.isCompleted,
'Please ensure ensureInitialized is called prior to getting a remote config value',
);
return storage.activeConfig?[key] ??
RemoteConfigValue(
_defaultConfig[key].mapNullable((v) => '$v'),
ValueSource.valueDefault,
);
}

/// Sets the [RemoteConfigSettings] for the current instance.
Future<void> setConfigSettings(
RemoteConfigSettings remoteConfigSettings,
) async {
assert(!remoteConfigSettings.fetchTimeout.isNegative);
assert(!remoteConfigSettings.minimumFetchInterval.isNegative);
// To be consistent with iOS fetchTimeout is set to the default
// 1 minute (60 seconds) if an attempt is made to set it to zero seconds.
if (remoteConfigSettings.fetchTimeout.inSeconds == 0) {
remoteConfigSettings.fetchTimeout = const Duration(seconds: 60);
}
_settings = remoteConfigSettings;
}

/// Sets the default parameter values for the current instance.
Future<void> setDefaults(Map<String, dynamic> defaultConfig) async {
_defaultConfig = {...defaultConfig};
}

/// Sets values to be immediately available
void setInitialValues({Map? remoteConfigValues}) {
if (remoteConfigValues == null) {
return;
}
final fetchTimeout = Duration(seconds: remoteConfigValues['fetchTimeout']);
final minimumFetchInterval =
Duration(seconds: remoteConfigValues['minimumFetchInterval']);
final lastFetchMillis = remoteConfigValues['lastFetchTime'];
final lastFetchStatus = remoteConfigValues['lastFetchStatus'];

_settings = RemoteConfigSettings(
fetchTimeout: fetchTimeout,
minimumFetchInterval: minimumFetchInterval,
);

storageCache.setLastFetchTime(
DateTime.fromMillisecondsSinceEpoch(lastFetchMillis),
);
storageCache.setLastFetchStatus(_parseFetchStatus(lastFetchStatus));
storageCache.setActiveConfig(
_parseParameters(remoteConfigValues['parameters']),
);
}

RemoteConfigFetchStatus _parseFetchStatus(String? status) {
try {
return status.mapNullable(RemoteConfigFetchStatus.values.byName) ??
RemoteConfigFetchStatus.noFetchYet;
} on Exception {
return RemoteConfigFetchStatus.noFetchYet;
}
}

Map<String, RemoteConfigValue> _parseParameters(
Map<dynamic, dynamic> rawParameters,
) {
final parameters = <String, RemoteConfigValue>{};
for (final key in rawParameters.keys) {
final rawValue = rawParameters[key] as Map;
parameters[key] = RemoteConfigValue(
rawValue['value'],
_parseValueSource(rawValue['source']),
);
}
return parameters;
}

ValueSource _parseValueSource(String? sourceStr) {
switch (sourceStr) {
case 'remote':
return ValueSource.valueRemote;
case 'default':
return ValueSource.valueDefault;
case 'static':
return ValueSource.valueStatic;
default:
return ValueSource.valueStatic;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2022 Invertase Limited. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file.
// ignore_for_file: public_member_api_docs, library_private_types_in_public_api

part of '../../firebase_remote_config_dart.dart';

@visibleForTesting
class RemoteConfigApiClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

On Auth, I made sure that the API client class is separate from the package class, so it won't be part of, and the API class on itself is a library. I did it this way cause I wanted to make sure that API internals stay hidden and I don't use its types outside accidentally (in the Desktop package), and control the visibility for package users. Check here, maybe we try to follow the same design?

RemoteConfigApiClient(
this.projectId,
this.namespace,
this.apiKey,
this.appId,
this.storage,
this.storageCache,
);
late final httpClient = clientViaApiKey(apiKey);
late final remoteConfigClient = api.FirebaseRemoteConfigApi(httpClient);

final _RemoteConfigStorage storage;
final _RemoteConfigStorageCache storageCache;
final String projectId;
final String appId;
final String namespace;
final String apiKey;

bool isCachedDataFresh(
Duration cacheMaxAge,
DateTime? lastSuccessfulFetchTimestamp,
) {
if (lastSuccessfulFetchTimestamp == null) {
return false;
}

final cacheAgeMillis = DateTime.now().millisecondsSinceEpoch -
lastSuccessfulFetchTimestamp.millisecondsSinceEpoch;
return cacheAgeMillis <= cacheMaxAge.inMilliseconds;
}

Future<Map> fetch({
String? eTag,
required Duration cacheMaxAge,
}) async {
final lastSuccessfulFetchTimestamp = storage.lastFetchTime;
final lastSuccessfulFetchResponse =
storage.getLastSuccessfulFetchResponse();

if (lastSuccessfulFetchResponse != null &&
isCachedDataFresh(cacheMaxAge, lastSuccessfulFetchTimestamp)) {
return lastSuccessfulFetchResponse;
}

final response = await remoteConfigClient.projects.namespaces.fetch(
api.FetchRemoteConfigRequest(
appId: appId,
appInstanceId: '1', // TODO: get from installations
sdkVersion: '0.1.0', // TODO: Sync with pubspec
),
projectId,
namespace,
);

storageCache.setLastFetchTime(DateTime.now());

storage.setLastSuccessfulFetchResponse(response.entries ?? {});
return response.entries ?? {};
}
}
Loading