Skip to content

Commit d2dfb16

Browse files
authored
Setup A/B testing for unlock relax sound messages (#507)
1 parent 6af6923 commit d2dfb16

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+578
-84
lines changed

bin/firebase_admin/upload_cache.json

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@
5959
"../../firestore_storages/relax_sounds/melody/bamboo_windchime.svg": "668b5af16732c98fed203044329e7c57",
6060
"../../firestore_storages/relax_sounds/melody/bamboo_windchime.txt": "5ee0172cca71bb966c16a165a7cb4fe9",
6161
"../../firestore_storages/relax_sounds/melody/bamboo_windchime.wav": "120f35dc35338ce5f8e1a18d87ed5511",
62-
"../../firestore_storages/relax_sounds/melody/music_box.svg": "f57cdea5e51e80ed544041da0f9c61b1",
63-
"../../firestore_storages/relax_sounds/melody/music_box.txt": "57eb01d59ec6efa62fb05bd99ba1ea5c",
64-
"../../firestore_storages/relax_sounds/melody/music_box.wav": "b769cf7361a40392acf305be7a68968c",
6562
"../../firestore_storages/relax_sounds/melody/singing_bowl.svg": "d855be3d08182e2df94e4ffff742cbc9",
6663
"../../firestore_storages/relax_sounds/melody/singing_bowl.txt": "f1b88c0e4b755d506cafd6488b0111cd",
6764
"../../firestore_storages/relax_sounds/melody/singing_bowl.wav": "ce3ec3bce2f60a3293bcc06f95ecb6da",
@@ -71,11 +68,18 @@
7168
"../../firestore_storages/relax_sounds/melody/wind_chime.svg": "e8ac013f3dab601ac2144a9efb2a4d73",
7269
"../../firestore_storages/relax_sounds/melody/wind_chime.txt": "24ee7bb1eba1c0c26ce3c27724c646b0",
7370
"../../firestore_storages/relax_sounds/melody/wind_chime.wav": "1d3f3e270bd5cb057e912eb4cb8dbf83",
74-
"../../firestore_storages/relax_sounds/musics/morning_due.svg": "8c03870421aba0491c1a58c496b73c2d",
75-
"../../firestore_storages/relax_sounds/musics/morning_due.txt": "e3b0c44298fc1c149afbf4c8996fb924",
71+
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.mp3": "04d6b7e986ec4094f0791c1bd89e8d3c",
72+
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.svg": "7df66b8d021c2ce726994a91506de2ee",
73+
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.txt": "ec7b8fb7b3d8518e3dce87d5084724be",
74+
"../../firestore_storages/relax_sounds/musics/music_box.svg": "f57cdea5e51e80ed544041da0f9c61b1",
75+
"../../firestore_storages/relax_sounds/musics/music_box.txt": "57eb01d59ec6efa62fb05bd99ba1ea5c",
76+
"../../firestore_storages/relax_sounds/musics/music_box.wav": "b769cf7361a40392acf305be7a68968c",
77+
"../../firestore_storages/relax_sounds/musics/serene_moments.mp3": "200078421b803ff0abf04490e1b7b805",
7678
"../../firestore_storages/relax_sounds/musics/serene_moments.svg": "3a69c5a14cf911b68a4dd5a02190cfa0",
7779
"../../firestore_storages/relax_sounds/musics/serene_moments.txt": "a2f02db0608c790ef8fed9b7f738527a",
78-
"../../firestore_storages/relax_sounds/musics/serene_moments.wav": "9e450a3d6ac2eda871b48d18a0459de0",
80+
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.mp3": "623541f7663eb848e8d3f410d446933e",
81+
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.svg": "c564b04b7ce9d917ef27a6fcc206d7f2",
82+
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.txt": "02a137a8f767924878dffc81a2ef00ec",
7983
"../../firestore_storages/relax_sounds/rainy/heavy_rain.svg": "0369af1683a87eb84dd0ec7fbb48530f",
8084
"../../firestore_storages/relax_sounds/rainy/heavy_rain.txt": "e102524aeb3a3ff019ca759e5440004a",
8185
"../../firestore_storages/relax_sounds/rainy/heavy_rain.wav": "9d51a5c88b19260b0ea27aa8690a4757",
@@ -99,15 +103,5 @@
99103
"../../firestore_storages/relax_sounds/water/ocean_waves.wav": "e3314b906cfa52065417f9c51c9b2f59",
100104
"../../firestore_storages/relax_sounds/water/river_stream.svg": "c138f14d67e6badf526d96d6bb0ca1d0",
101105
"../../firestore_storages/relax_sounds/water/river_stream.txt": "230636fa87deb1c789ce49711b7671c4",
102-
"../../firestore_storages/relax_sounds/water/river_stream.wav": "fd60b586ac2bc25d511a2c41ef7c6faa",
103-
"../../firestore_storages/relax_sounds/musics/serene_moments.mp3": "200078421b803ff0abf04490e1b7b805",
104-
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.mp3": "623541f7663eb848e8d3f410d446933e",
105-
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.svg": "c564b04b7ce9d917ef27a6fcc206d7f2",
106-
"../../firestore_storages/relax_sounds/musics/serene_piano_reflections.txt": "02a137a8f767924878dffc81a2ef00ec",
107-
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.mp3": "04d6b7e986ec4094f0791c1bd89e8d3c",
108-
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.svg": "7df66b8d021c2ce726994a91506de2ee",
109-
"../../firestore_storages/relax_sounds/musics/acoustic_guitar_duet.txt": "ec7b8fb7b3d8518e3dce87d5084724be",
110-
"../../firestore_storages/relax_sounds/musics/music_box.svg": "f57cdea5e51e80ed544041da0f9c61b1",
111-
"../../firestore_storages/relax_sounds/musics/music_box.txt": "57eb01d59ec6efa62fb05bd99ba1ea5c",
112-
"../../firestore_storages/relax_sounds/musics/music_box.wav": "b769cf7361a40392acf305be7a68968c"
106+
"../../firestore_storages/relax_sounds/water/river_stream.wav": "fd60b586ac2bc25d511a2c41ef7c6faa"
113107
}

bin/localization/data.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ button.grant_permission,Grant Permission,منح الإذن,Berechtigung erteilen
2727
button.import,Import,يستورد,Import,Importar,Importar,Importer,आयात,Impor,Importare,インポート,នាំចូលឯកសារ,가져오기,Import,Importar,Nhập khẩu,导入,Импорт
2828
button.info,Info,معلومات,Info,Información,Información,Informations,जानकारी,Informasi,Informazioni,情報,ព័ត៌មាន,정보,Informacje,Informações,Thông tin,信息,Инфо
2929
button.manage_pages,Manage pages,إدارة الصفحات,Seiten verwalten,Administrar páginas,Administrar páginas,Gérer les pages,पेज प्रबंधित करें,Kelola halaman,Gestisci le pagine,ページの管理,គ្រប់គ្រងទំព័រ,페이지 관리,Zarządzaj stronami,Gerenciar páginas,Quản lý trang,管理页面,Управление Страницами
30+
button.maybe_later,Maybe later,ربما في وقت لاحق,Vielleicht später,Quizás más tarde,Quizás más tarde,Peut-être plus tard,शायद बाद में,mungkin nanti,Forse più tardi,後で,ប្រហែលជាពេលក្រោយ,아마 나중에,Może później,Talvez mais tarde,Có thể sau này,也许以后,Vielleicht später
3031
button.more_options,More Options,المزيد من الخيارات,Weitere Optionen,Más opciones,Más opciones,Plus d'options,अधिक विकल्प,Opsi Lainnya,Più opzioni,その他のオプション,ជម្រើសច្រើនទៀត,더 많은 옵션,Więcej opcji,Mais opções,Tùy chọn khác,更多选项,Больше Опций
3132
button.move_to_bin,Move to bin,الانتقال إلى سلة المهملات,In den Papierkorb verschieben,Mover a la papelera,Mover a la papelera,Déplacer vers la corbeille,बिन में ले जाएँ,Pindah ke tempat sampah,Sposta nel cestino,ゴミ箱に移動,ផ្លាស់ទីទៅធុងសំរាម,휴지통으로 이동,Przenieś do kosza,Mover para a lixeira,Di chuyển vào thùng rác,移至垃圾箱,В Корзину
3233
button.move_to_bin_all,Move all to bins,انقل الكل إلى الصناديق,Alles in die Mülleimer verschieben,Mover todo a contenedores,Mover todo a contenedores,Déplacez tout dans les bacs,सभी को डिब्बे में ले जाएँ,Pindahkan semua ke tempat sampah,Sposta tutto nei contenitori,すべてをゴミ箱に移動,ផ្លាស់ទីទាំងអស់ទៅធុងសំរាម,모두 휴지통으로 이동,Przenieś wszystko do koszy,Mova tudo para as lixeiras,Di chuyển tất cả vào thùng,全部移至垃圾箱,Всё в корзину

lib/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class App extends StatelessWidget {
1919
path: 'translations',
2020
supportedLocales: kSupportedLocales,
2121
fallbackLocale: kFallbackLocale,
22+
startLocale: kFallbackLocale,
2223
child: AppTheme(
2324
builder: (context, preferences, theme, darkTheme, themeMode) {
2425
TextScaler textScaler = switch (preferences.fontSize) {

lib/core/objects/relax_sound_object.dart

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class RelaxSoundObject {
1212
final String artist;
1313
final String svgIconUrlPath;
1414
final FirestoreStorageBackground background;
15+
final bool free;
1516

1617
// mp3 or wave
1718
final String soundUrlPath;
@@ -25,6 +26,7 @@ class RelaxSoundObject {
2526
required this.svgIconUrlPath,
2627
required this.background,
2728
required this.soundUrlPath,
29+
this.free = false,
2830
this.dayColor = 3,
2931
}) : assert(dayColor >= 1 && dayColor <= 7);
3032

@@ -64,6 +66,7 @@ class RelaxSoundObject {
6466
svgIconUrlPath: '/relax_sounds/rainy/light_rain.svg',
6567
soundUrlPath: '/relax_sounds/rainy/light_rain.wav',
6668
dayColor: 3,
69+
free: true,
6770
),
6871
RelaxSoundObject(
6972
artist: '@InspectorJ',
@@ -101,6 +104,7 @@ class RelaxSoundObject {
101104
svgIconUrlPath: '/relax_sounds/water/ocean_waves.svg',
102105
soundUrlPath: '/relax_sounds/water/ocean_waves.wav',
103106
dayColor: 5,
107+
free: true,
104108
),
105109
RelaxSoundObject(
106110
artist: '@felix.blume',
@@ -138,6 +142,7 @@ class RelaxSoundObject {
138142
svgIconUrlPath: '/relax_sounds/animal/night_crickets.svg',
139143
soundUrlPath: '/relax_sounds/animal/night_crickets.wav',
140144
dayColor: 2,
145+
free: true,
141146
),
142147
RelaxSoundObject(
143148
artist: '@sacred_steel',
@@ -183,6 +188,7 @@ class RelaxSoundObject {
183188
svgIconUrlPath: '/relax_sounds/melody/wind_chime.svg',
184189
soundUrlPath: '/relax_sounds/melody/wind_chime.wav',
185190
dayColor: 5,
191+
free: true,
186192
),
187193
RelaxSoundObject(
188194
artist: '@LoopUdu',
@@ -220,6 +226,7 @@ class RelaxSoundObject {
220226
svgIconUrlPath: '/relax_sounds/fire/campfire.svg',
221227
soundUrlPath: '/relax_sounds/fire/campfire.wav',
222228
dayColor: 1,
229+
free: true,
223230
),
224231
RelaxSoundObject(
225232
artist: '@eclectic-kitty',
@@ -241,6 +248,7 @@ class RelaxSoundObject {
241248
svgIconUrlPath: '/relax_sounds/body/heartbeat.svg',
242249
soundUrlPath: '/relax_sounds/body/heartbeat.wav',
243250
dayColor: 7,
251+
free: true,
244252
),
245253
];
246254
}
@@ -254,29 +262,22 @@ class RelaxSoundObject {
254262
svgIconUrlPath: '/relax_sounds/activity/typing.svg',
255263
soundUrlPath: '/relax_sounds/activity/typing.wav',
256264
dayColor: 7,
265+
free: true,
257266
),
258267
];
259268
}
260269

261270
static List<RelaxSoundObject> musicSounds() {
262271
return [
263-
RelaxSoundObject(
264-
artist: '@voyouz',
265-
translationKey: 'sounds.music_box',
266-
background: FirestoreStorageBackground.music_notes_on_heart_shaped_paper,
267-
svgIconUrlPath: '/relax_sounds/musics/music_box.svg',
268-
soundUrlPath: '/relax_sounds/musics/music_box.wav',
269-
dayColor: 1,
270-
),
271272
RelaxSoundObject(
272273
artist: '@graham_makes',
273274
translationKey: 'sounds.acoustic_guitar_duet',
274275
background: FirestoreStorageBackground.music_notes_on_heart_shaped_paper,
275276
svgIconUrlPath: '/relax_sounds/musics/acoustic_guitar_duet.svg',
276277
soundUrlPath: '/relax_sounds/musics/acoustic_guitar_duet.mp3',
277278
dayColor: 3,
279+
free: true,
278280
),
279-
280281
RelaxSoundObject(
281282
artist: '@Matio888',
282283
translationKey: 'sounds.serene_piano_reflections',
@@ -293,6 +294,14 @@ class RelaxSoundObject {
293294
soundUrlPath: '/relax_sounds/musics/serene_moments.mp3',
294295
dayColor: 1,
295296
),
297+
RelaxSoundObject(
298+
artist: '@voyouz',
299+
translationKey: 'sounds.music_box',
300+
background: FirestoreStorageBackground.music_notes_on_heart_shaped_paper,
301+
svgIconUrlPath: '/relax_sounds/musics/music_box.svg',
302+
soundUrlPath: '/relax_sounds/musics/music_box.wav',
303+
dayColor: 1,
304+
),
296305
];
297306
}
298307

lib/core/services/firestore_storage_service.dart

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,19 @@ class FirestoreStorageService {
143143
// "/relax_sounds/water/ocean_waves-130d1d326a06fe0f21d4650a4f7065b7.txt",
144144
// "/relax_sounds/water/droplets-ec36e00209a8cece33eef6f5c3f80e61.txt",
145145
// };
146-
final validPaths = (await hash ?? {}).values.map((e) => path.normalize(e.toString())).toSet();
146+
final validBasenames = (await hash ?? {}).values.map((e) => path.basename(e.toString())).toSet();
147147

148148
final files = await downloadDir.list(recursive: true).where((entity) => entity is File).toList();
149149
final deletedFiles = <String>[];
150150

151151
for (final fileEntity in files) {
152-
// Converts absolute path to relative path (e.g., '/full/path/to/app/relax_sounds/rainy.txt' -> 'relax_sounds/rainy.txt')
153-
final relativePath = path.relative(fileEntity.path, from: downloadDir.path);
152+
final filename = path.basename(fileEntity.path);
154153

155-
// Normalizes path separators (e.g., 'relax_sounds\\rainy.txt' -> 'relax_sounds/rainy.txt')
156-
final normalizedPath = path.normalize(relativePath);
157-
158-
if (validPaths.contains(normalizedPath)) continue;
154+
if (validBasenames.contains(filename)) continue;
159155

160156
try {
161157
await fileEntity.delete();
162-
deletedFiles.add(relativePath);
158+
deletedFiles.add(filename);
163159
} catch (e, s) {
164160
AppLogger.error(
165161
'$runtimeType#cleanupUnusedFiles failed to delete file ${fileEntity.path}: $e',

lib/core/services/remote_config/remote_config_service.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class RemoteConfigService {
1818
surveyUrl,
1919
twitterUrl,
2020
websiteUrl,
21+
relaxSoundUnlockSheetVariant,
2122
];
2223

2324
FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance;
@@ -96,6 +97,14 @@ class RemoteConfigService {
9697
'https://storypad.me',
9798
);
9899

100+
// A/B Testing
101+
102+
static const relaxSoundUnlockSheetVariant = _RemoteConfigObject<String>(
103+
'RELAX_SOUND_UNLOCK_SHEET_VARIANT',
104+
_RemoteConfigValueType.string,
105+
'variant_1',
106+
);
107+
99108
Future<void> initialize() async {
100109
try {
101110
await remoteConfig.setConfigSettings(

lib/providers/in_app_purchase_provider.dart

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:io';
22
import 'package:adaptive_dialog/adaptive_dialog.dart';
33
import 'package:easy_localization/easy_localization.dart';
4-
import 'package:flutter/foundation.dart';
54
import 'package:flutter/material.dart';
65
import 'package:flutter/services.dart';
76
import 'package:provider/provider.dart';
@@ -29,7 +28,7 @@ class InAppPurchaseProvider extends ChangeNotifier {
2928

3029
bool get relaxSound => isActive(AppProduct.relax_sounds.productIdentifier);
3130
bool get template => isActive(AppProduct.templates.productIdentifier);
32-
bool get periodCalendar => kDebugMode || isActive(AppProduct.period_calendar.productIdentifier);
31+
bool get periodCalendar => isActive(AppProduct.period_calendar.productIdentifier);
3332

3433
DateTime? _rewardExpiredAt;
3534
List<String>? _rewardAddOns;
@@ -38,6 +37,7 @@ class InAppPurchaseProvider extends ChangeNotifier {
3837
List<String> get rewardAddOns => _rewardAddOns ?? [];
3938

4039
CustomerInfo? _customerInfo;
40+
List<StoreProduct>? storeProducts;
4141

4242
InAppPurchaseProvider(BuildContext context) {
4343
_initialize(context).then((_) async {
@@ -63,6 +63,29 @@ class InAppPurchaseProvider extends ChangeNotifier {
6363
}
6464
}
6565

66+
StoreProduct? getProduct(String productIdentifier) {
67+
return storeProducts?.where((storeProduct) => storeProduct.identifier == productIdentifier).firstOrNull;
68+
}
69+
70+
Future<List<StoreProduct>?> fetchAndCacheProducts({
71+
required String debugSource,
72+
}) async {
73+
try {
74+
storeProducts = kIAPEnabled
75+
? await Purchases.getProducts(AppProduct.productIdentifiers, productCategory: ProductCategory.nonSubscription)
76+
: [];
77+
} on PlatformException catch (e, s) {
78+
AppLogger.error(
79+
'$runtimeType#fetchProducts($debugSource) PlatformException - code: ${e.code}, message: ${e.message}, details: ${e.details}',
80+
stackTrace: s,
81+
);
82+
} catch (e, s) {
83+
AppLogger.error('$runtimeType#fetchProducts($debugSource) error: ${e.toString()}', stackTrace: s);
84+
}
85+
86+
return storeProducts;
87+
}
88+
6689
Future<void> revalidateCustomerInfo(BuildContext context) async {
6790
if (!kIAPEnabled) return;
6891

@@ -141,7 +164,7 @@ class InAppPurchaseProvider extends ChangeNotifier {
141164
Future<bool> purchase(
142165
BuildContext context,
143166
String productIdentifier,
144-
Future<void> Function() onPurchased,
167+
Future<void> Function()? onPurchased,
145168
) async {
146169
if (!kIAPEnabled) return false;
147170

@@ -163,7 +186,7 @@ class InAppPurchaseProvider extends ChangeNotifier {
163186
try {
164187
PurchaseResult result = await Purchases.purchase(PurchaseParams.storeProduct(storeProduct));
165188
_customerInfo = result.customerInfo;
166-
if (isActive(productIdentifier)) await onPurchased();
189+
if (isActive(productIdentifier)) await onPurchased?.call();
167190
notifyListeners();
168191
} on PlatformException catch (e, s) {
169192
PurchasesErrorCode errorCode = PurchasesErrorHelper.getErrorCode(e);

lib/providers/relax_sounds_provider.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import 'package:flutter/material.dart';
22
import 'package:just_audio/just_audio.dart';
3+
import 'package:provider/provider.dart';
34
import 'package:storypad/core/databases/models/relex_sound_mix_model.dart';
45
import 'package:storypad/core/mixins/debounched_callback.dart';
56
import 'package:storypad/core/objects/relax_sound_object.dart';
7+
import 'package:storypad/core/services/messenger_service.dart';
68
import 'package:storypad/core/services/multi_audio_notification_service.dart';
79
import 'package:storypad/core/services/multi_audio_player_service.dart';
810
import 'package:storypad/core/services/relax_sound_timer_service.dart';
11+
import 'package:storypad/core/types/app_product.dart';
12+
import 'package:storypad/providers/in_app_purchase_provider.dart';
13+
import 'package:storypad/views/add_ons/add_ons_view.dart';
14+
import 'package:storypad/widgets/bottom_sheets/sp_unlock_relax_sounds_sheet.dart';
915

1016
class RelaxSoundsProvider extends ChangeNotifier with DebounchedCallback {
1117
Map<String, RelaxSoundObject> get relaxSounds => RelaxSoundObject.defaultSoundsList();
@@ -83,8 +89,14 @@ class RelaxSoundsProvider extends ChangeNotifier with DebounchedCallback {
8389

8490
Future<void> toggleSound(
8591
RelaxSoundObject sound, {
92+
required BuildContext context,
8693
double? initialVolume,
8794
}) async {
95+
Feedback.forTap(context);
96+
97+
final iapProvider = context.read<InAppPurchaseProvider>();
98+
if (!sound.free && !iapProvider.relaxSound) return showUnlockSheet(context);
99+
88100
if (isSoundSelected(sound)) {
89101
await audioPlayersService.removeAnAudio(sound.soundUrlPath);
90102
} else {
@@ -97,6 +109,34 @@ class RelaxSoundsProvider extends ChangeNotifier with DebounchedCallback {
97109
refreshCanSaveMix();
98110
}
99111

112+
Future<void> showUnlockSheet(BuildContext context) async {
113+
final iapProvider = context.read<InAppPurchaseProvider>();
114+
115+
if (iapProvider.storeProducts == null) {
116+
await MessengerService.of(context).showLoading(
117+
debugSource: '$runtimeType#toggleSound',
118+
future: () => iapProvider.fetchAndCacheProducts(debugSource: '$runtimeType#toggleSound'),
119+
);
120+
}
121+
122+
if (!context.mounted) return;
123+
String? displayPrice = iapProvider.getProduct(AppProduct.relax_sounds.productIdentifier)?.priceString;
124+
125+
return SpUnlockRelaxSoundsSheet(
126+
displayPrice: displayPrice ?? '\$1',
127+
onUnlock: (sheetContext) async {
128+
await Navigator.maybePop(sheetContext);
129+
if (!context.mounted) return;
130+
131+
AddOnsRoute.pushAndNavigateTo(
132+
product: AppProduct.relax_sounds,
133+
context: context,
134+
fullscreenDialog: true,
135+
);
136+
},
137+
).show(context: context);
138+
}
139+
100140
Future<void> playAll({
101141
required Map<RelaxSoundObject, double?> soundWithInitialVolume,
102142
}) async {

0 commit comments

Comments
 (0)