Skip to content

Commit

Permalink
feat(remote-config): support for custom signals (#8274)
Browse files Browse the repository at this point in the history
  • Loading branch information
russellwheatley authored Feb 9, 2025
1 parent 5fb2037 commit 94c6a27
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 0 deletions.
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 customSignals, Promise promise) {
module
.setCustomSignals(appName, customSignals.toHashMap())
.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
23 changes: 23 additions & 0 deletions packages/remote-config/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,26 @@ 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.
* There are additional limitations on key and value length, for a full description see https://firebase.google.com/docs/remote-config/parameters?template_type=client#custom_signal_conditions
* Failing to stay within these limitations will result in a silent API failure with only a warning in device logs
*/

export interface CustomSignals {
[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 @@ import { getApp } from '@react-native-firebase/app';
* @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 setDefaultsFromResource(remoteConfig, resourceName) {
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) {
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);
});
},
};

0 comments on commit 94c6a27

Please sign in to comment.