Skip to content

Commit 7cf248a

Browse files
feat(remote-config): custom signals support (#17053)
1 parent 68ed56b commit 7cf248a

File tree

12 files changed

+185
-2
lines changed

12 files changed

+185
-2
lines changed

packages/firebase_remote_config/firebase_remote_config/android/src/main/java/io/flutter/plugins/firebase/firebaseremoteconfig/FirebaseRemoteConfigPlugin.java

+38
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.google.firebase.remoteconfig.ConfigUpdate;
1717
import com.google.firebase.remoteconfig.ConfigUpdateListener;
1818
import com.google.firebase.remoteconfig.ConfigUpdateListenerRegistration;
19+
import com.google.firebase.remoteconfig.CustomSignals;
1920
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
2021
import com.google.firebase.remoteconfig.FirebaseRemoteConfigClientException;
2122
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
@@ -137,6 +138,36 @@ private FirebaseRemoteConfig getRemoteConfig(Map<String, Object> arguments) {
137138
return FirebaseRemoteConfig.getInstance(app);
138139
}
139140

141+
private Task<Void> setCustomSignals(
142+
FirebaseRemoteConfig remoteConfig, Map<String, Object> customSignalsArguments) {
143+
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
144+
cachedThreadPool.execute(
145+
() -> {
146+
try {
147+
CustomSignals.Builder customSignals = new CustomSignals.Builder();
148+
for (Map.Entry<String, Object> entry : customSignalsArguments.entrySet()) {
149+
Object value = entry.getValue();
150+
if (value instanceof String) {
151+
customSignals.put(entry.getKey(), (String) value);
152+
} else if (value instanceof Long) {
153+
customSignals.put(entry.getKey(), (Long) value);
154+
} else if (value instanceof Integer) {
155+
customSignals.put(entry.getKey(), ((Integer) value).longValue());
156+
} else if (value instanceof Double) {
157+
customSignals.put(entry.getKey(), (Double) value);
158+
} else if (value == null) {
159+
customSignals.put(entry.getKey(), null);
160+
}
161+
}
162+
Tasks.await(remoteConfig.setCustomSignals(customSignals.build()));
163+
taskCompletionSource.setResult(null);
164+
} catch (Exception e) {
165+
taskCompletionSource.setException(e);
166+
}
167+
});
168+
return taskCompletionSource.getTask();
169+
}
170+
140171
@Override
141172
public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result result) {
142173
Task<?> methodCallTask;
@@ -192,6 +223,13 @@ public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result re
192223
methodCallTask = Tasks.forResult(configProperties);
193224
break;
194225
}
226+
case "RemoteConfig#setCustomSignals":
227+
{
228+
Map<String, Object> customSignals =
229+
Objects.requireNonNull(call.argument("customSignals"));
230+
methodCallTask = setCustomSignals(remoteConfig, customSignals);
231+
break;
232+
}
195233
default:
196234
{
197235
result.notImplemented();

packages/firebase_remote_config/firebase_remote_config/example/android/settings.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pluginManagement {
1818

1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21-
id "com.android.application" version "7.3.0" apply false
21+
id "com.android.application" version "8.1.0" apply false
2222
// START: FlutterFire Configuration
2323
id "com.google.gms.google-services" version "4.3.15" apply false
2424
// END: FlutterFire Configuration

packages/firebase_remote_config/firebase_remote_config/ios/firebase_remote_config/Sources/firebase_remote_config/FLTFirebaseRemoteConfigPlugin.m

+16
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,28 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
106106
[self setDefaults:call.arguments withMethodCallResult:methodCallResult];
107107
} else if ([@"RemoteConfig#getProperties" isEqualToString:call.method]) {
108108
[self getProperties:call.arguments withMethodCallResult:methodCallResult];
109+
} else if ([@"RemoteConfig#setCustomSignals" isEqualToString:call.method]) {
110+
[self setCustomSignals:call.arguments withMethodCallResult:methodCallResult];
109111
} else {
110112
methodCallResult.success(FlutterMethodNotImplemented);
111113
}
112114
}
113115

114116
#pragma mark - Remote Config API
117+
- (void)setCustomSignals:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
118+
FIRRemoteConfig *remoteConfig = [self getFIRRemoteConfigFromArguments:arguments];
119+
NSDictionary *customSignals = arguments[@"customSignals"];
120+
121+
[remoteConfig setCustomSignals:customSignals
122+
withCompletion:^(NSError *_Nullable error) {
123+
if (error != nil) {
124+
result.error(nil, nil, nil, error);
125+
} else {
126+
result.success(nil);
127+
}
128+
}];
129+
}
130+
115131
- (void)ensureInitialized:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
116132
FIRRemoteConfig *remoteConfig = [self getFIRRemoteConfigFromArguments:arguments];
117133
[remoteConfig ensureInitializedWithCompletionHandler:^(NSError *initializationError) {

packages/firebase_remote_config/firebase_remote_config/lib/src/firebase_remote_config.dart

+16
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,20 @@ class FirebaseRemoteConfig extends FirebasePluginPlatform {
168168
Stream<RemoteConfigUpdate> get onConfigUpdated {
169169
return _delegate.onConfigUpdated;
170170
}
171+
172+
/// Changes the custom signals for this FirebaseRemoteConfig instance
173+
/// Custom signals are subject to limits on the size of key/value pairs and the total number of signals.
174+
/// Any calls that exceed these limits will be discarded.
175+
/// If a key already exists, the value is overwritten. Setting the value of a custom signal to null un-sets the signal.
176+
/// The signals will be persisted locally on the client.
177+
Future<void> setCustomSignals(Map<String, Object?> customSignals) {
178+
customSignals.forEach((key, value) {
179+
// Apple will not trigger exception for boolean because it is represented as a number in objective-c so we assert early for all platforms
180+
assert(
181+
value is String || value is num || value == null,
182+
'Invalid value type "${value.runtimeType}" for key "$key". Only strings, numbers, or null are supported.',
183+
);
184+
});
185+
return _delegate.setCustomSignals(customSignals);
186+
}
171187
}

packages/firebase_remote_config/firebase_remote_config_platform_interface/lib/src/method_channel/method_channel_firebase_remote_config.dart

+15
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,19 @@ class MethodChannelFirebaseRemoteConfig extends FirebaseRemoteConfigPlatform {
306306
});
307307
return _onConfigUpdatedStream!;
308308
}
309+
310+
@override
311+
Future<void> setCustomSignals(Map<String, Object?> customSignals) {
312+
try {
313+
return channel.invokeMethod<void>(
314+
'RemoteConfig#setCustomSignals',
315+
<String, dynamic>{
316+
'appName': app.name,
317+
'customSignals': customSignals,
318+
},
319+
);
320+
} catch (exception, stackTrace) {
321+
convertPlatformException(exception, stackTrace);
322+
}
323+
}
309324
}

packages/firebase_remote_config/firebase_remote_config_platform_interface/lib/src/platform_interface/platform_interface_firebase_remote_config.dart

+9
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,13 @@ abstract class FirebaseRemoteConfigPlatform extends PlatformInterface {
181181
Stream<RemoteConfigUpdate> get onConfigUpdated {
182182
throw UnimplementedError('onConfigUpdated getter not implemented');
183183
}
184+
185+
/// Changes the custom signals for this FirebaseRemoteConfig instance
186+
/// Custom signals are subject to limits on the size of key/value pairs and the total number of signals.
187+
/// Any calls that exceed these limits will be discarded.
188+
/// If a key already exists, the value is overwritten. Setting the value of a custom signal to null un-sets the signal.
189+
/// The signals will be persisted locally on the client.
190+
Future<void> setCustomSignals(Map<String, Object?> customSignals) {
191+
throw UnimplementedError('setCustomSignals() is not implemented');
192+
}
184193
}

packages/firebase_remote_config/firebase_remote_config_web/lib/firebase_remote_config_web.dart

+8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:firebase_core/firebase_core.dart';
66
import 'package:firebase_core_web/firebase_core_web.dart';
77
import 'package:firebase_core_web/firebase_core_web_interop.dart'
88
as core_interop;
9+
import 'package:firebase_remote_config_web/src/internals.dart';
910
import 'package:firebase_remote_config_platform_interface/firebase_remote_config_platform_interface.dart';
1011
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
1112

@@ -185,4 +186,11 @@ class FirebaseRemoteConfigWeb extends FirebaseRemoteConfigPlatform {
185186
Stream<RemoteConfigUpdate> get onConfigUpdated {
186187
throw UnsupportedError('onConfigUpdated is not supported for web');
187188
}
189+
190+
@override
191+
Future<void> setCustomSignals(Map<String, Object?> customSignals) {
192+
return convertWebExceptions(
193+
() => _delegate.setCustomSignals(customSignals),
194+
);
195+
}
188196
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2021, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:firebase_core/firebase_core.dart';
6+
import 'package:_flutterfire_internals/_flutterfire_internals.dart'
7+
as internals;
8+
9+
/// Will return a [FirebaseException] from a thrown web error.
10+
/// Any other errors will be propagated as normal.
11+
R convertWebExceptions<R>(R Function() cb) {
12+
return internals.guardWebExceptions(
13+
cb,
14+
plugin: 'firebase_remote_config',
15+
codeParser: (code) => code.replaceFirst('remote_config/', ''),
16+
);
17+
}

packages/firebase_remote_config/firebase_remote_config_web/lib/src/interop/firebase_remote_config.dart

+6
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ class RemoteConfig
152152
.toJS,
153153
);
154154
}
155+
156+
Future<void> setCustomSignals(Map<String, Object?> customSignals) {
157+
return remote_config_interop
158+
.setCustomSignals(jsObject, customSignals.jsify()! as JSObject)
159+
.toDart;
160+
}
155161
}
156162

157163
ValueSource getSource(String source) {

packages/firebase_remote_config/firebase_remote_config_web/lib/src/interop/firebase_remote_config_interop.dart

+7-1
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,17 @@ external JSString getString(RemoteConfigJsImpl remoteConfig, JSString key);
5151
@staticInterop
5252
external ValueJsImpl getValue(RemoteConfigJsImpl remoteConfig, JSString key);
5353

54-
// TODO - api to be implemented
5554
@JS()
5655
@staticInterop
5756
external JSPromise isSupported();
5857

58+
@JS()
59+
@staticInterop
60+
external JSPromise setCustomSignals(
61+
RemoteConfigJsImpl remoteConfig,
62+
JSObject customSignals,
63+
);
64+
5965
@JS()
6066
@staticInterop
6167
external void setLogLevel(RemoteConfigJsImpl remoteConfig, JSString logLevel);

packages/firebase_remote_config/firebase_remote_config_web/pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ environment:
1010
flutter: '>=3.22.0'
1111

1212
dependencies:
13+
_flutterfire_internals: ^1.3.50
1314
firebase_core: ^3.10.1
1415
firebase_core_web: ^2.18.2
1516
firebase_remote_config_platform_interface: ^1.4.49

tests/integration_test/firebase_remote_config/firebase_remote_config_e2e_test.dart

+51
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,57 @@ void main() {
155155
expect(FirebaseRemoteConfig.instance.getInt('does-not-exist'), 0);
156156
expect(FirebaseRemoteConfig.instance.getDouble('does-not-exist'), 0.0);
157157
});
158+
159+
group('setCustomSignals()', () {
160+
test('valid signal values; `string`, `number` & `null`', () async {
161+
const signals = <String, Object?>{
162+
'signal1': 'string',
163+
'signal2': 204953,
164+
'signal3': 3.24,
165+
'signal4': null,
166+
};
167+
168+
await FirebaseRemoteConfig.instance.setCustomSignals(signals);
169+
});
170+
171+
test('invalid signal values throws assertion', () async {
172+
const signals = <String, Object?>{
173+
'signal1': true,
174+
};
175+
176+
await expectLater(
177+
() => FirebaseRemoteConfig.instance.setCustomSignals(signals),
178+
throwsA(isA<AssertionError>()),
179+
);
180+
181+
const signals2 = <String, Object?>{
182+
'signal1': [1, 2, 3],
183+
};
184+
185+
await expectLater(
186+
() => FirebaseRemoteConfig.instance.setCustomSignals(signals2),
187+
throwsA(isA<AssertionError>()),
188+
);
189+
190+
const signals3 = <String, Object?>{
191+
'signal1': {'key': 'value'},
192+
};
193+
194+
await expectLater(
195+
() => FirebaseRemoteConfig.instance.setCustomSignals(signals3),
196+
throwsA(isA<AssertionError>()),
197+
);
198+
199+
const signals4 = <String, Object?>{
200+
'signal1': false,
201+
};
202+
203+
await expectLater(
204+
() => FirebaseRemoteConfig.instance.setCustomSignals(signals4),
205+
throwsA(isA<AssertionError>()),
206+
);
207+
});
208+
});
158209
},
159210
);
160211
}

0 commit comments

Comments
 (0)