Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions modules/ensemble/lib/framework/data_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import 'package:ensemble/framework/secrets.dart';
import 'package:ensemble/framework/stub/auth_context_manager.dart';
import 'package:ensemble/framework/stub/oauth_controller.dart';
import 'package:ensemble/framework/stub/token_manager.dart';
import 'package:ensemble/framework/stub/remote_config.dart';
import 'package:ensemble/framework/storage_manager.dart';
import 'package:ensemble/framework/widget/view_util.dart';
import 'package:ensemble/host_platform_manager.dart';
Expand Down Expand Up @@ -465,6 +466,7 @@ class NativeInvokable extends ActionInvokable {
Map<String, Function> getters() {
return {
'storage': () => storage,
'remoteConfig': () => RemoteConfigInvokable(),
'user': () => UserInfo(),
'formatter': () => Formatter(),
'utils': () => _EnsembleUtils(),
Expand Down Expand Up @@ -690,6 +692,53 @@ class EnsembleSocketInvokable with Invokable {
}
}

/// Invokable for Remote Config
class RemoteConfigInvokable with Invokable {
static RemoteConfig get _provider =>
GetIt.instance.isRegistered<RemoteConfig>()
? GetIt.instance<RemoteConfig>()
: RemoteConfigStub();

@override
dynamic getProperty(dynamic prop) =>
_provider.getValue(prop?.toString() ?? '', null);

@override
Map<String, Function> getters() => {};

@override
Map<String, Function> methods() {
return {
'get': (dynamic key, [dynamic defaultValue]) {
final k = key?.toString() ?? '';
final def = defaultValue ?? '';
return _provider.getValue(k, def);
},
'getBool': (dynamic key, [bool defaultValue = false]) =>
_provider.getValue(key?.toString() ?? '', defaultValue) as bool,
'getInt': (dynamic key, [int defaultValue = 0]) =>
_provider.getValue(key?.toString() ?? '', defaultValue) as int,
'getDouble': (dynamic key, [double defaultValue = 0.0]) =>
_provider.getValue(key?.toString() ?? '', defaultValue) as double,
'getString': (dynamic key, [String defaultValue = '']) =>
_provider.getValue(key?.toString() ?? '', defaultValue) as String,
'all': () => _provider.getAllValues(),
'info': () => _provider.getInfo(),
'refresh': () => _provider.refresh(),
'setDefaults': (dynamic defaults) {
final map = Utils.getMap(defaults);
if (map == null) {
return null;
}
return _provider.setDefaults(Map<String, dynamic>.from(map));
},
};
}

@override
Map<String, Function> setters() => {};
}

/// Singleton handling user storage
class EnsembleStorage with Invokable {
static final EnsembleStorage _instance = EnsembleStorage._internal();
Expand Down
52 changes: 52 additions & 0 deletions modules/ensemble/lib/framework/stub/remote_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// Abstract provider for Remote Config values.
///
/// The real implementation is provided by the [ensemble_remote_config] module
/// when enabled via [EnsembleModules] in the starter; otherwise a stub is used.
abstract class RemoteConfig {
dynamic getValue(String key, dynamic defaultValue);

/// Return all currently known values, using the same typing rules as
/// [getValue] with a `null` default (i.e. minimal inference).
Map<String, dynamic> getAllValues();

/// Metadata about the current Remote Config state (debugging only).
Map<String, dynamic> getInfo();

/// Manually trigger a refresh/fetch of Remote Config values.
///
/// This is intended for developer tools or long‑lived sessions, and is
/// typically called from expressions as `ensemble.remoteConfig.refresh()`.
Future<void> refresh();

/// Optionally register a map of default values.
///
/// This is primarily intended for use from Dart (e.g. starter wiring), but
/// is also exposed in expressions as `ensemble.remoteConfig.setDefaults({...})`
/// for advanced scenarios.
Future<void> setDefaults(Map<String, dynamic> defaults);
}

/// Stub used when the ensemble_remote_config module is not enabled.
/// Always returns [defaultValue] so expressions like ensemble.remoteConfig.my_key or ensemble.remoteConfig.get('key', 'default')
/// work without throwing.
class RemoteConfigStub implements RemoteConfig {
@override
dynamic getValue(String key, dynamic defaultValue) => defaultValue;

@override
Map<String, dynamic> getAllValues() => const {};

@override
Map<String, dynamic> getInfo() =>
const {'initialized': false, 'source': 'stub'};

@override
Future<void> refresh() async {
// no-op in stub
}

@override
Future<void> setDefaults(Map<String, dynamic> defaults) async {
// no-op in stub
}
}
96 changes: 96 additions & 0 deletions modules/ensemble_remote_config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Ensemble Remote Config

Ensemble module for Firebase Remote Config. Use it for A/B testing, feature flags, and runtime configuration in screens and expressions.

## What it does

- Fetches and activates Remote Config in the background when the module is registered.
- Exposes Remote Config on the ensemble object like storage: `ensemble.remoteConfig.my_key` (or `ensemble.remoteConfig.get('key', 'default')` for a custom default).

## Enabling the module

Remote Config is **off** by default. To turn it on:

1. **Starter `pubspec.yaml`**
Uncomment the dependency:

```yaml
ensemble_remote_config:
git:
url: https://github.com/EnsembleUI/ensemble.git
ref: <ensemble-version>
path: modules/ensemble_remote_config
```

2. **Starter `lib/generated/ensemble_modules.dart`**
- Set `useRemoteConfig = true`.
- Uncomment: `import 'package:ensemble_remote_config/remote_config.dart';`
- Uncomment: `GetIt.I.registerSingleton<RemoteConfig>(RemoteConfigImpl());`

3. **Firebase**
Ensure Firebase is set up (e.g. `GoogleService-Info.plist` / `google-services.json`). With `useRemoteConfig = true`, `Firebase.initializeApp()` is already triggered by the starter’s init.

### Optional environment variables

You can tune Remote Config fetch behavior via environment variables:

- `remote_config_fetch_timeout` — fetch timeout in seconds (default `10`).
- `remote_config_minimum_fetch_interval` — minimum fetch interval in seconds (default `3600`, i.e. 1 hour).

Example:

```yaml
environmentVariables:
remote_config_fetch_timeout: 10
remote_config_minimum_fetch_interval: 3600
```

Notes:

- These values are applied when Remote Config is initialized **and** before each
subsequent `fetchAndActivate()` / `ensemble.remoteConfig.refresh()`.
- On the very first app start, Ensemble config may not be loaded yet when RC
initializes, so the defaults can be used for the first fetch; any later call
to `ensemble.remoteConfig.refresh()` re-reads and reapplies the env values.
- For local debugging you can temporarily set `remote_config_minimum_fetch_interval: 0` to avoid throttling, but you should use a higher value in production.

## Usage in YAML

In any screen or expression, use the `ensemble` object

- **Property access** — `ensemble.remoteConfig.my_key` returns a minimally typed value
- `"true"/"false"` → `bool`
- numeric string → `num`
- otherwise a `String` (or `null` if the key is missing/empty).
- **Custom default and type** — `ensemble.remoteConfig.get('my_key', false)` uses the
default's type (here `bool`) and falls back to that default on errors.
- **Typed helpers** — these are just wrappers around `get` with a typed default:
- `ensemble.remoteConfig.getBool('flag', false)`
- `ensemble.remoteConfig.getInt('max_items', 10)`
- `ensemble.remoteConfig.getDouble('ratio', 0.5)`
- `ensemble.remoteConfig.getString('cta_text', 'Try it now')`

### Defaults

- `ensemble.remoteConfig.setDefaults({ flag: true, max_items: 10 })` registers app‑side
defaults with Firebase Remote Config.
- **Intended use**: call once during app startup before the screens that depend on these keys are built.
- Changing defaults at runtime does **not automatically update existing widgets**; you may need to rebuild the screen or navigate away/back to see the new default take effect.

### Debug helpers

For debugging and introspection (e.g. a developer screen):

- `ensemble.remoteConfig.all()` → map of all keys to their current values.
- `ensemble.remoteConfig.info()` → metadata such as `initialized`, `lastFetchStatus`,
`lastFetchTime`, and fetch/interval settings.
- `ensemble.remoteConfig.refresh()` → manually trigger a re‑fetch/activate of
Remote Config values (using Firebase's built‑in throttling and current env
settings for timeout/interval).

## Debug logging

In debug builds, the module logs to `debugPrint` when:

- Fetch/activate fails (with exception and stack).
- `getValue` returns the default because the service isn’t initialized, the key is empty, or an error occurred.
Loading