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(remote-config): support for custom signals #8274

Merged
merged 10 commits into from
Feb 9, 2025
5 changes: 5 additions & 0 deletions packages/remote-config/__tests__/remote-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
setDefaults,
setDefaultsFromResource,
onConfigUpdated,
setCustomSignals,
} from '../lib';

describe('remoteConfig()', function () {
Expand Down Expand Up @@ -216,5 +217,9 @@ describe('remoteConfig()', function () {
it('`onConfigUpdated` function is properly exposed to end user', function () {
expect(onConfigUpdated).toBeDefined();
});

it('`setCustomSignals` function is properly exposed to end user', function () {
expect(setCustomSignals).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.remoteconfig.CustomSignals;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigInfo;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
Expand Down Expand Up @@ -199,6 +201,40 @@ private Bundle convertRemoteConfigValue(FirebaseRemoteConfigValue value) {
return convertedConfigBundle;
}

Task<Void> setCustomSignals(String appName, HashMap<String, Object> customSignalsMap) {
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();

FirebaseApp firebaseApp = FirebaseApp.getInstance(appName);

getExecutor().execute(
() -> {
try {
CustomSignals.Builder customSignals = new CustomSignals.Builder();
for (Map.Entry<String, Object> entry : customSignalsMap.entrySet()) {
Object value = entry.getValue();
if (value instanceof String) {
customSignals.put(entry.getKey(), (String) value);
} else if (value instanceof Long) {
customSignals.put(entry.getKey(), (Long) value);
} else if (value instanceof Integer) {
customSignals.put(entry.getKey(), ((Integer) value).longValue());
} else if (value instanceof Double) {
customSignals.put(entry.getKey(), (Double) value);
} else if (value == null) {
customSignals.put(entry.getKey(), null);
}
}

Tasks.await(FirebaseRemoteConfig.getInstance(firebaseApp).setCustomSignals(customSignals.build()));
taskCompletionSource.setResult(null);
} catch (Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

public Map<String, Object> getConstantsForApp(String appName) {
Map<String, Object> appConstants = new HashMap<>();
FirebaseRemoteConfigInfo remoteConfigInfo =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,20 @@ public void removeConfigUpdateRegistration(String appName) {
}
}

@ReactMethod
public void setCustomSignals(String appName, ReadableMap setCustomSignals, Promise promise) {
module
.setCustomSignals(appName, setCustomSignals.toHashMap())
russellwheatley marked this conversation as resolved.
Show resolved Hide resolved
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
promise.resolve(resultWithConstants(task.getResult()));
} else {
rejectPromiseWithExceptionMap(promise, task.getException());
}
});
}

private WritableMap resultWithConstants(Object result) {
Map<String, Object> responseMap = new HashMap<>(2);
responseMap.put("result", result);
Expand Down
41 changes: 41 additions & 0 deletions packages/remote-config/e2e/config.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -954,5 +954,46 @@ describe('remoteConfig()', function () {
// console.log('checking listener functionality across javascript layer reload');
});
});

describe('setCustomSignals()', function () {
it('should resolve with valid signal value; `string`, `number` or `null`', async function () {
const { setCustomSignals, getRemoteConfig } = remoteConfigModular;
const remoteConfig = getRemoteConfig(firebase.app());
// native SDKs just ignore invalid key/values (e.g. too long) and just log warning
const signals = {
string: 'string',
number: 123,
double: 123.456,
null: null,
};

const result = await setCustomSignals(remoteConfig, signals);
should(result).equal(null);
});

it('should reject with invalid signal value', async function () {
const { setCustomSignals, getRemoteConfig } = remoteConfigModular;
const remoteConfig = getRemoteConfig(firebase.app());

const invalidSignals = [
{ signal1: true },
{ signal1: [1, 2, 3] },
{ signal1: { key: 'value' } },
{ signal1: false },
];

for (const signals of invalidSignals) {
try {
await setCustomSignals(remoteConfig, signals);
throw new Error('Expected setCustomSignals to throw an error');
} catch (error) {
error.message.should.containEql(
'firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal',
);
}
}
return Promise.resolve();
});
});
});
});
18 changes: 18 additions & 0 deletions packages/remote-config/ios/RNFBConfig/RNFBConfigModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,24 @@ - (void)invalidate {
}
}

RCT_EXPORT_METHOD(setCustomSignals
: (FIRApp *)firebaseApp
: (NSDictionary *)customSignals
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
[[FIRRemoteConfig remoteConfigWithApp:firebaseApp]
setCustomSignals:customSignals
withCompletion:^(NSError *_Nullable error) {
if (error != nil) {
[RNFBSharedUtils rejectPromiseWithNSError:reject error:error];
} else {
resolve(nil);
}
}];

resolve([self resultWithConstants:[NSNull null] firebaseApp:firebaseApp]);
}

#pragma mark -
#pragma mark Internal Helper Methods

Expand Down
21 changes: 21 additions & 0 deletions packages/remote-config/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,24 @@ export function onConfigUpdated(
remoteConfig: RemoteConfig,
callback: (config: ConfigValues) => void,
): () => void;

/**
* Defines the type for representing custom signals and their values.
* The values in CustomSignals must be one of the following types: string, number, or null.
*/
russellwheatley marked this conversation as resolved.
Show resolved Hide resolved

export interface CustomSignals {
mikehardy marked this conversation as resolved.
Show resolved Hide resolved
[key: string]: string | number | null;
}

/**
* Sets the custom signals for the app instance.
* @param {RemoteConfig} remoteConfig - RemoteConfig instance
* @param {CustomSignals} customSignals - CustomSignals
* @returns {Promise<void>}
*/

export declare function setCustomSignals(
remoteConfig: RemoteConfig,
customSignals: CustomSignals,
): Promise<void>;
18 changes: 18 additions & 0 deletions packages/remote-config/lib/modular/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* @typedef {import('..').FirebaseRemoteConfigTypes.ConfigValues} ConfigValues
* @typedef {import('..').FirebaseRemoteConfigTypes.LastFetchStatusType} LastFetchStatusType
* @typedef {import('..').FirebaseRemoteConfigTypes.RemoteConfigLogLevel} RemoteConfigLogLevel
* @typedef {import('.').CustomSignals} CustomSignals
*/

/**
Expand Down Expand Up @@ -243,3 +244,20 @@
export function onConfigUpdated(remoteConfig, callback) {
return remoteConfig.onConfigUpdated(callback);
}

/**
* Sets the custom signals for the app instance.
* @param {RemoteConfig} remoteConfig - RemoteConfig instance
* @param {CustomSignals} customSignals - CustomSignals
* @returns {Promise<void>}
*/
export async function setCustomSignals(remoteConfig, customSignals) {

Check warning on line 254 in packages/remote-config/lib/modular/index.js

View check run for this annotation

Codecov / codecov/patch

packages/remote-config/lib/modular/index.js#L254

Added line #L254 was not covered by tests
for (const [key, value] of Object.entries(customSignals)) {
if (typeof value !== 'string' && typeof value !== 'number' && value !== null) {
throw new Error(
`firebase.remoteConfig().setCustomSignals(): Invalid type for custom signal '${key}': ${typeof value}. Expected 'string', 'number', or 'null'.`,
);
}
}
return remoteConfig._promiseWithConstants(remoteConfig.native.setCustomSignals(customSignals));
}
8 changes: 8 additions & 0 deletions packages/remote-config/lib/web/RNFBConfigModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
fetchConfig,
getAll,
makeIDBAvailable,
setCustomSignals,
} from '@react-native-firebase/app/lib/internal/web/firebaseRemoteConfig';
import { guard } from '@react-native-firebase/app/lib/internal/web/utils';

Expand Down Expand Up @@ -113,4 +114,11 @@ export default {
return resultAndConstants(remoteConfig, null);
});
},
setCustomSignals(appName, customSignals) {
return guard(async () => {
const remoteConfig = getRemoteConfigInstanceForApp(appName);
await setCustomSignals(remoteConfig, customSignals);
return resultAndConstants(remoteConfig, null);
});
},
};
Loading