Skip to content

Commit e7fefe5

Browse files
authored
Simplify block activation overrides to scoped key only (#1759)
* Simplify block override lookup to superblock/block key only * Unify superblock and block overrides under one config key * Unify superblock and block activation checks into one API
1 parent 04ce757 commit e7fefe5

File tree

5 files changed

+132
-61
lines changed

5 files changed

+132
-61
lines changed

mobile-app/lib/service/firebase/remote_config_service.dart

Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:developer';
33

44
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
55
import 'package:firebase_remote_config/firebase_remote_config.dart';
6+
import 'package:flutter/foundation.dart';
67
import 'package:flutter/services.dart';
78
import 'package:freecodecamp/utils/upgrade_controller.dart';
89
import 'package:upgrader/upgrader.dart';
@@ -11,13 +12,20 @@ class RemoteConfigService {
1112
static final RemoteConfigService _instance = RemoteConfigService._internal();
1213

1314
static final remoteConfig = FirebaseRemoteConfig.instance;
14-
static const _superblockActivationKey = 'superblock_activation_overrides';
15-
static const _blockActivationKey = 'block_activation_overrides';
15+
static const _activationOverridesKey = 'activation_overrides';
16+
final String Function(String key) _getConfigString;
1617

1718
factory RemoteConfigService() {
1819
return _instance;
1920
}
2021

22+
@visibleForTesting
23+
factory RemoteConfigService.withConfigReader(
24+
String Function(String key) getConfigString,
25+
) {
26+
return RemoteConfigService._internal(getConfigString: getConfigString);
27+
}
28+
2129
Future<void> init() async {
2230
try {
2331
await remoteConfig.setConfigSettings(
@@ -28,8 +36,7 @@ class RemoteConfigService {
2836
);
2937
await remoteConfig.setDefaults({
3038
'min_app_version': '7.3.0',
31-
_superblockActivationKey: '{}',
32-
_blockActivationKey: '{}',
39+
_activationOverridesKey: '{}',
3340
});
3441

3542
await remoteConfig.fetchAndActivate();
@@ -59,71 +66,36 @@ class RemoteConfigService {
5966
}
6067
}
6168

62-
RemoteConfigService._internal();
63-
64-
bool isSuperBlockActive(
65-
String dashedName, {
66-
required bool fallbackValue,
67-
}) {
68-
final override = getSuperBlockActivationOverride(dashedName);
69-
return override ?? fallbackValue;
70-
}
71-
72-
bool? getSuperBlockActivationOverride(String dashedName) {
73-
return _getOverrideValue(
74-
configKey: _superblockActivationKey,
75-
key: dashedName,
76-
);
77-
}
69+
RemoteConfigService._internal({
70+
String Function(String key)? getConfigString,
71+
}) : _getConfigString = getConfigString ?? remoteConfig.getString;
7872

79-
bool isBlockActive({
73+
bool isActive({
8074
required String superBlockDashedName,
81-
required String blockDashedName,
82-
bool fallbackValue = true,
75+
String? blockDashedName,
76+
required bool fallbackValue,
8377
}) {
84-
final override = getBlockActivationOverride(
78+
final override = getActivationOverride(
8579
superBlockDashedName: superBlockDashedName,
8680
blockDashedName: blockDashedName,
8781
);
8882
return override ?? fallbackValue;
8983
}
9084

91-
bool? getBlockActivationOverride({
85+
bool? getActivationOverride({
9286
required String superBlockDashedName,
93-
required String blockDashedName,
87+
String? blockDashedName,
9488
}) {
95-
final overrides = _getOverridesMap(_blockActivationKey);
96-
if (overrides == null) {
97-
return null;
98-
}
99-
100-
final superBlockScopedKey = '$superBlockDashedName/$blockDashedName';
101-
final directScopedOverride = overrides[superBlockScopedKey];
102-
if (directScopedOverride is bool) {
103-
return directScopedOverride;
104-
}
105-
106-
final scopedOverrides = overrides[superBlockDashedName];
107-
if (scopedOverrides is Map) {
108-
final nestedOverride = scopedOverrides[blockDashedName];
109-
if (nestedOverride is bool) {
110-
return nestedOverride;
111-
}
112-
}
113-
114-
final directOverride = overrides[blockDashedName];
115-
if (directOverride is bool) {
116-
return directOverride;
117-
}
118-
119-
return null;
89+
final overrideKey = blockDashedName == null
90+
? superBlockDashedName
91+
: '$superBlockDashedName/$blockDashedName';
92+
return _getOverrideValue(key: overrideKey);
12093
}
12194

12295
bool? _getOverrideValue({
123-
required String configKey,
12496
required String key,
12597
}) {
126-
final overrides = _getOverridesMap(configKey);
98+
final overrides = _getOverridesMap();
12799
if (overrides == null) {
128100
return null;
129101
}
@@ -136,9 +108,9 @@ class RemoteConfigService {
136108
return null;
137109
}
138110

139-
Map<String, dynamic>? _getOverridesMap(String configKey) {
111+
Map<String, dynamic>? _getOverridesMap() {
140112
try {
141-
final String rawConfig = remoteConfig.getString(configKey);
113+
final String rawConfig = _getConfigString(_activationOverridesKey);
142114
if (rawConfig.trim().isEmpty) {
143115
return null;
144116
}
@@ -150,7 +122,7 @@ class RemoteConfigService {
150122
return decoded;
151123
} catch (exception) {
152124
log(
153-
'Invalid remote config for $configKey. '
125+
'Invalid remote config for $_activationOverridesKey. '
154126
'Ignoring overrides: $exception',
155127
);
156128
return null;

mobile-app/lib/ui/views/learn/chapter/chapter_viewmodel.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ class ChapterViewModel extends BaseViewModel {
9898
final List<Module> filteredModules = chapter.modules
9999
?.map((module) {
100100
final List<Block> filteredBlocks = (module.blocks ?? [])
101-
.where((block) => _remoteConfigService.isBlockActive(
101+
.where((block) => _remoteConfigService.isActive(
102102
superBlockDashedName: block.superBlock.dashedName,
103103
blockDashedName: block.dashedName,
104+
fallbackValue: true,
104105
))
105106
.toList();
106107

mobile-app/lib/ui/views/learn/landing/landing_viewmodel.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,8 @@ class LearnLandingViewModel extends BaseViewModel {
225225
}
226226

227227
final fallbackValue = showAllSB || superBlock['public'] == true;
228-
final override =
229-
_remoteConfigService.getSuperBlockActivationOverride(
230-
superBlock['dashedName'],
228+
final override = _remoteConfigService.getActivationOverride(
229+
superBlockDashedName: superBlock['dashedName'],
231230
);
232231
final isPublic = override ?? fallbackValue;
233232

mobile-app/lib/ui/views/learn/superblock/superblock_viewmodel.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ class SuperBlockViewModel extends BaseViewModel {
9898

9999
List<Block> _filterAvailableBlocks(List<Block> blocks) {
100100
return blocks.map((block) {
101-
final isActive = _remoteConfigService.isBlockActive(
101+
final isActive = _remoteConfigService.isActive(
102102
superBlockDashedName: block.superBlock.dashedName,
103103
blockDashedName: block.dashedName,
104+
fallbackValue: true,
104105
);
105106

106107
if (isActive) return block;
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:freecodecamp/service/firebase/remote_config_service.dart';
3+
4+
void main() {
5+
TestWidgetsFlutterBinding.ensureInitialized();
6+
7+
group('RemoteConfigService activation overrides', () {
8+
test('reads superblock override from activation_overrides', () {
9+
final service = RemoteConfigService.withConfigReader(
10+
(_) => '{"javascript-algorithms-and-data-structures": false}',
11+
);
12+
13+
expect(
14+
service.getActivationOverride(
15+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
16+
),
17+
isFalse,
18+
);
19+
20+
expect(
21+
service.isActive(
22+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
23+
fallbackValue: true,
24+
),
25+
isFalse,
26+
);
27+
});
28+
29+
test('reads block override from superblock/block scoped key', () {
30+
final service = RemoteConfigService.withConfigReader(
31+
(_) =>
32+
'{"javascript-algorithms-and-data-structures/basic-javascript": false}',
33+
);
34+
35+
expect(
36+
service.getActivationOverride(
37+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
38+
blockDashedName: 'basic-javascript',
39+
),
40+
isFalse,
41+
);
42+
43+
expect(
44+
service.isActive(
45+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
46+
blockDashedName: 'basic-javascript',
47+
fallbackValue: true,
48+
),
49+
isFalse,
50+
);
51+
});
52+
53+
test('returns fallback when override is missing', () {
54+
final service = RemoteConfigService.withConfigReader((_) => '{}');
55+
56+
expect(
57+
service.isActive(
58+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
59+
fallbackValue: true,
60+
),
61+
isTrue,
62+
);
63+
64+
expect(
65+
service.isActive(
66+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
67+
blockDashedName: 'basic-javascript',
68+
fallbackValue: false,
69+
),
70+
isFalse,
71+
);
72+
});
73+
74+
test('ignores invalid override config payloads', () {
75+
final invalidShapeService = RemoteConfigService.withConfigReader(
76+
(_) => '["not-a-map"]',
77+
);
78+
79+
expect(
80+
invalidShapeService.getActivationOverride(
81+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
82+
),
83+
isNull,
84+
);
85+
86+
final nonBooleanValueService = RemoteConfigService.withConfigReader(
87+
(_) => '{"javascript-algorithms-and-data-structures": "false"}',
88+
);
89+
90+
expect(
91+
nonBooleanValueService.getActivationOverride(
92+
superBlockDashedName: 'javascript-algorithms-and-data-structures',
93+
),
94+
isNull,
95+
);
96+
});
97+
});
98+
}

0 commit comments

Comments
 (0)