Skip to content

Commit

Permalink
Add remote config service
Browse files Browse the repository at this point in the history
  • Loading branch information
Madumo committed Oct 2, 2024
1 parent 4c2e09d commit 4499cbb
Show file tree
Hide file tree
Showing 6 changed files with 321 additions and 0 deletions.
1 change: 1 addition & 0 deletions template/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ module.exports = {
'react/jsx-boolean-value': 2,
'react/react-in-jsx-scope': 0,
'react-native/no-inline-styles': 0,
'no-dupe-class-members': 0, // incompatible with typescript method overload
},
};
2 changes: 2 additions & 0 deletions template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@klarna/react-native-vector-drawable": "^0.5.0",
"@react-native-community/geolocation": "^3.1.0",
"@react-native-community/netinfo": "^11.2.0",
"@react-native-firebase/remote-config": "^21.0.0",
"@react-navigation/native": "^6.1.9",
"@react-navigation/native-stack": "^6.9.17",
"@tanstack/react-query": "^5.12.2",
Expand Down Expand Up @@ -47,6 +48,7 @@
"react-native-splash-screen": "^3.3.0",
"react-query-kit": "^2.0.10",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"tsyringe": "^4.8.0"
},
"devDependencies": {
Expand Down
29 changes: 29 additions & 0 deletions template/src/hooks/use-remote-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
import { useService } from './use-service';
import RemoteConfigService, {
RemoteConfig,
RemoteConfigKey,
} from '~/services/remote-config';

export function useRemoteConfig<K extends RemoteConfigKey>(
key: K
): RemoteConfig[K] {
const remoteConfigService = useService(RemoteConfigService);
const [value, setValue] = useState(() =>
remoteConfigService.getConfigValue(key)
);

useEffect(() => {
setValue(remoteConfigService.getConfigValue(key));

const unsubscribe = remoteConfigService.subscribe(key, (newValue) =>
setValue(newValue)
);

return () => {
unsubscribe();
};
}, [key, remoteConfigService]);

return value;
}
137 changes: 137 additions & 0 deletions template/src/services/remote-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import remoteConfig from '@react-native-firebase/remote-config';
import { BehaviorSubject } from 'rxjs';
import { singleton } from 'tsyringe';
import { Duration } from '~/utils/Duration';

export type RemoteConfig = {
some_feature_flag: boolean;
};

export type RemoteConfigValue = boolean | number | string;

export type RemoteConfigKey = keyof RemoteConfig;

export type RemoteConfigSubjects = {
[K in RemoteConfigKey]: BehaviorSubject<RemoteConfig[K]>;
};

export type RemoteConfigSubscribeCallback<K extends RemoteConfigKey> = (
value: RemoteConfig[K]
) => void;

export type RemoteConfigUnsubscribe = () => void;

export type RemoteConfigSettings = {
timeout: Duration;
cache: Duration;
};

@singleton()
export default class RemoteConfigService {
private static readonly DEFAULTS: RemoteConfig = {
some_feature_flag: true,
};

private static readonly SETTINGS: RemoteConfigSettings = {
timeout: Duration.from('seconds', 30),
cache: Duration.from('minutes', 10),
};

private readonly configs: RemoteConfigSubjects = {
some_feature_flag: new BehaviorSubject(
RemoteConfigService.DEFAULTS.some_feature_flag
),
};

constructor() {
this.init();
}

private removeOnConfigUpdated = remoteConfig().onConfigUpdated(() => {
this.updateConfigs();
});

private async init() {
await this.setConfigSettings();
await this.setDefaults();
await this.fetchAndActivate();
this.updateConfigs();
}

private fetchAndActivate() {
return remoteConfig().fetchAndActivate();
}

private setConfigSettings() {
return remoteConfig().setConfigSettings({
minimumFetchIntervalMillis:
RemoteConfigService.SETTINGS.cache.to('milliseconds'),
fetchTimeMillis: RemoteConfigService.SETTINGS.timeout.to('milliseconds'),
});
}

private setDefaults() {
return remoteConfig().setDefaults(RemoteConfigService.DEFAULTS);
}

private async updateConfigs() {
Object.keys(RemoteConfigService.DEFAULTS).forEach((key) => {
this.updateConfig(key as RemoteConfigKey);
});
}

private updateConfig(key: RemoteConfigKey) {
const $config: BehaviorSubject<any> = this.configs[key];
const value = this.getRemoteValue(key);

if ($config.getValue() !== value) {
$config.next(value);
}
}

private getRemoteValue<K extends RemoteConfigKey>(key: K): RemoteConfig[K] {
switch (typeof RemoteConfigService.DEFAULTS[key]) {
case 'boolean':
return this.getRemoteBoolean(key) as any;

case 'number':
return this.getRemoteNumber(key) as any;

case 'string':
return this.getRemoteString(key) as any;
}
}

private getRemoteBoolean(key: RemoteConfigKey) {
return remoteConfig().getValue(key).asBoolean();
}

private getRemoteNumber(key: RemoteConfigKey) {
return remoteConfig().getValue(key).asNumber();
}

private getRemoteString(key: RemoteConfigKey) {
return remoteConfig().getValue(key).asString();
}

getConfigValue<K extends RemoteConfigKey>(key: K): RemoteConfig[K] {
return this.configs[key].getValue();
}

subscribe<K extends RemoteConfigKey>(
key: K,
callback: RemoteConfigSubscribeCallback<K>
): RemoteConfigUnsubscribe {
const subscription = this.configs[key].subscribe((value) => {
callback(value);
});

return () => {
subscription.unsubscribe();
};
}

destroy() {
this.removeOnConfigUpdated();
}
}
135 changes: 135 additions & 0 deletions template/src/utils/Duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
export type DurationUnit =
| 'nanoseconds'
| 'microseconds'
| 'milliseconds'
| 'seconds'
| 'minutes'
| 'hours'
| 'days';

export type DurationComponents = Partial<Record<DurationUnit, number>>;

const NANOSECONDS = 1;
const MICROSECONDS = NANOSECONDS * 1000;
const MILLISECONDS = MICROSECONDS * 1000;
const SECONDS = MILLISECONDS * 1000;
const MINUTES = SECONDS * 60;
const HOURS = MINUTES * 60;
const DAYS = HOURS * 24;

const MULTIPLIERS: Record<DurationUnit, number> = {
nanoseconds: NANOSECONDS,
microseconds: MICROSECONDS,
milliseconds: MILLISECONDS,
seconds: SECONDS,
minutes: MINUTES,
hours: HOURS,
days: DAYS,
};

const UNITS: DurationUnit[] = [
'nanoseconds',
'microseconds',
'milliseconds',
'seconds',
'minutes',
'hours',
'days',
];

export class Duration {
private constructor(private readonly _nanoseconds: number) {}

static from(components: DurationComponents): Duration;
static from(unit: DurationUnit, value: number): Duration;
static from(
componentsOrUnit: DurationComponents | DurationUnit,
value?: number
): Duration {
if (typeof componentsOrUnit === 'object') {
return Duration.fromComponents(componentsOrUnit);
}

return Duration.fromUnit(componentsOrUnit, value!);
}

private static fromComponents(components: DurationComponents) {
let nanoseconds = 0;

for (const unit of UNITS) {
const multiplier = MULTIPLIERS[unit];
const value = components[unit] ?? 0;
nanoseconds += value * multiplier;
}

return new Duration(nanoseconds);
}

private static fromUnit(unit: DurationUnit, value: number) {
return new Duration(value * MULTIPLIERS[unit]);
}

to(unit: DurationUnit) {
return this._nanoseconds / MULTIPLIERS[unit];
}

add(duration: Duration): Duration;
add(components: DurationComponents): Duration;
add(unit: DurationUnit, value: number): Duration;
add(
durationOrcomponentsOrUnit: Duration | DurationComponents | DurationUnit,
value?: number
): Duration {
if (durationOrcomponentsOrUnit instanceof Duration) {
return this.addDuration(durationOrcomponentsOrUnit);
}

if (typeof durationOrcomponentsOrUnit === 'object') {
return this.addComponents(durationOrcomponentsOrUnit);
}

return this.addUnit(durationOrcomponentsOrUnit, value!);
}

private addDuration(duration: Duration) {
return new Duration(this._nanoseconds + duration._nanoseconds);
}

private addComponents(components: DurationComponents) {
return this.addDuration(Duration.fromComponents(components));
}

private addUnit(unit: DurationUnit, value: number) {
return new Duration(this._nanoseconds + value * MULTIPLIERS[unit]);
}

sub(duration: Duration): Duration;
sub(components: DurationComponents): Duration;
sub(unit: DurationUnit, value: number): Duration;
sub(
durationOrComponentsOrUnit: Duration | DurationComponents | DurationUnit,
value?: number
): Duration {
if (durationOrComponentsOrUnit instanceof Duration) {
return this.subDuration(durationOrComponentsOrUnit);
}

if (typeof durationOrComponentsOrUnit === 'object') {
return this.subComponents(durationOrComponentsOrUnit);
}

return this.subUnit(durationOrComponentsOrUnit, value!);
}

private subDuration(duration: Duration) {
return new Duration(this._nanoseconds - duration._nanoseconds);
}

private subComponents(components: DurationComponents) {
return this.subDuration(Duration.fromComponents(components));
}

private subUnit(unit: DurationUnit, value: number) {
return new Duration(this._nanoseconds - value * MULTIPLIERS[unit]);
}
}
17 changes: 17 additions & 0 deletions template/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1947,6 +1947,11 @@
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.2.0.tgz#2f0808ee8559905beed631bc79abd9d88fd9e8b7"
integrity sha512-Zpq8/BTBUUI4kito41VUtn/dGQZU1/SCNHvPZkjlUo41Mec38z13ntxeaIC+2LP5Dw0xKpZ/Lql47BZeXk+Ocg==

"@react-native-firebase/remote-config@^21.0.0":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@react-native-firebase/remote-config/-/remote-config-21.0.0.tgz#d2881bd54c28ac107fbffe1be638e5a3c16c4bb8"
integrity sha512-B/u87cXFS0XoM67LJVSnl3F8m/SL4J7F53Tm/PqFHEDsuhMdgWBBjN/bpdwSxkj4Fz16DGK4wZSpvfmZTpTNDA==

"@react-native/[email protected]":
version "0.73.1"
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.73.1.tgz#e2a6b73b16c183a270f338dc69c36039b3946e85"
Expand Down Expand Up @@ -6970,6 +6975,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"

rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"

safe-array-concat@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
Expand Down Expand Up @@ -7538,6 +7550,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.1, tslib@^2.6
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==

tslib@^2.1.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==

tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
Expand Down

0 comments on commit 4499cbb

Please sign in to comment.