Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save develop4God/983e8cac4467d1b3728beb79fe036616 to your computer and use it in GitHub Desktop.

Select an option

Save develop4God/983e8cac4467d1b3728beb79fe036616 to your computer and use it in GitHub Desktop.
Análisis completo de develop4God/Devocional_nuevo (rama: copilot/implement-user-churn-prediction) (PR: #133) + 1 módulo(s) core
ANÁLISIS COMPLETO DE REPOSITORIO - RAMA: copilot/implement-user-churn-prediction
PROYECTO PRINCIPAL: Devocional_nuevo
CARPETAS ANALIZADAS: lib, i18n, test + pubspec.yml
================================================================================
📁 ESTRUCTURA DEL PROYECTO PRINCIPAL:
========================================
📁 i18n/
├─ en.json (31999 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/en.json
├─ es.json (34265 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/es.json
├─ fr.json (35998 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/fr.json
├─ ja.json (39006 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/ja.json
├─ pt.json (34278 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/pt.json
📁 lib/
📁 adapters/
├─ http_client_adapter.dart (2522 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/adapters/http_client_adapter.dart
├─ storage_adapter.dart (3862 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/adapters/storage_adapter.dart
📁 blocs/
📁 bible_version/
├─ bible_version_bloc.dart (10608 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_bloc.dart
├─ bible_version_event.dart (3095 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_event.dart
├─ bible_version_state.dart (3233 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_state.dart
📁 devocionales/
├─ devocionales_bloc.dart (2007 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_bloc.dart
├─ devocionales_event.dart (366 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_event.dart
├─ devocionales_state.dart (603 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_state.dart
📁 onboarding/
├─ onboarding_bloc.dart (36501 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_bloc.dart
├─ onboarding_event.dart (2200 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_event.dart
├─ onboarding_models.dart (7002 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_models.dart
├─ onboarding_state.dart (3448 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_state.dart
📁 theme/
├─ theme_bloc.dart (4668 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_bloc.dart
├─ theme_event.dart (943 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_event.dart
├─ theme_repository.dart (2537 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_repository.dart
├─ theme_state.dart (2771 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_state.dart
├─ backup_bloc.dart (16740 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_bloc.dart
├─ backup_event.dart (2172 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_event.dart
├─ backup_state.dart (3707 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_state.dart
├─ prayer_bloc.dart (9851 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_bloc.dart
├─ prayer_event.dart (1104 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_event.dart
├─ prayer_state.dart (1974 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_state.dart
├─ thanksgiving_bloc.dart (8048 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_bloc.dart
├─ thanksgiving_event.dart (890 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_event.dart
├─ thanksgiving_state.dart (1185 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_state.dart
📁 controllers/
├─ audio_controller.dart (22409 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/controllers/audio_controller.dart
├─ tts_audio_controller.dart (2771 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/controllers/tts_audio_controller.dart
📁 debug/
├─ debug_settings_section.dart (4916 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/debug_settings_section.dart
├─ debug_settings_section_stub.dart (901 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/debug_settings_section_stub.dart
├─ test_badges_page.dart (6800 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/test_badges_page.dart
📁 extensions/
├─ string_extensions.dart (477 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/extensions/string_extensions.dart
📁 models/
├─ badge_model.dart (1932 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/badge_model.dart
├─ devocional_model.dart (3567 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/devocional_model.dart
├─ prayer_model.dart (4418 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/prayer_model.dart
├─ spiritual_stats_model.dart (9413 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/spiritual_stats_model.dart
├─ thanksgiving_model.dart (1962 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/thanksgiving_model.dart
📁 pages/
📁 onboarding/
├─ onboarding_backup_configuration_page.dart (5798 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_backup_configuration_page.dart
├─ onboarding_complete_page.dart (17604 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_complete_page.dart
├─ onboarding_flow.dart (11434 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_flow.dart
├─ onboarding_theme_selection_page.dart (12079 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_theme_selection_page.dart
├─ onboarding_welcome_page.dart (4448 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_welcome_page.dart
├─ about_page.dart (7837 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/about_page.dart
├─ application_language_page.dart (15474 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/application_language_page.dart
├─ backup_settings_page.dart (30391 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/backup_settings_page.dart
├─ bible_reader_page.dart (40770 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/bible_reader_page.dart
├─ bible_versions_manager_page.dart (12247 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/bible_versions_manager_page.dart
├─ contact_page.dart (12777 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/contact_page.dart
├─ devocionales_page.dart (49929 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/devocionales_page.dart
├─ favorites_page.dart (8785 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/favorites_page.dart
├─ notification_config_page.dart (19157 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/notification_config_page.dart
├─ prayers_page.dart (41475 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/prayers_page.dart
├─ progress_page.dart (22049 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/progress_page.dart
├─ settings_page.dart (25743 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/settings_page.dart
📁 providers/
├─ bible_selected_version_provider.dart (11378 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/bible_selected_version_provider.dart
├─ devocional_provider.dart (31372 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/devocional_provider.dart
├─ localization_provider.dart (2004 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/localization_provider.dart
📁 services/
📁 tts/
├─ bible_text_formatter.dart (7949 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/bible_text_formatter.dart
├─ i_tts_service.dart (1675 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/i_tts_service.dart
├─ voice_settings_service.dart (24885 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/voice_settings_service.dart
├─ churn_prediction_service.dart (17914 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/churn_prediction_service.dart
├─ compression_service.dart (3961 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/compression_service.dart
├─ connectivity_service.dart (2667 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/connectivity_service.dart
├─ devocionales_tracking.dart (9613 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/devocionales_tracking.dart
├─ google_drive_auth_service.dart (10482 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/google_drive_auth_service.dart
├─ google_drive_backup_service.dart (28750 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/google_drive_backup_service.dart
├─ in_app_review_service.dart (14148 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/in_app_review_service.dart
├─ localization_service.dart (7800 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/localization_service.dart
├─ notification_service.dart (32285 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/notification_service.dart
├─ onboarding_service.dart (6082 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/onboarding_service.dart
├─ remote_badge_service.dart (6565 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/remote_badge_service.dart
├─ service_locator.dart (6124 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/service_locator.dart
├─ spiritual_stats_service.dart (28179 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/spiritual_stats_service.dart
├─ tts_service.dart (19840 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts_service.dart
├─ update_service.dart (5551 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/update_service.dart
📁 utils/
├─ bubble_constants.dart (14334 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/bubble_constants.dart
├─ churn_monitoring_helper.dart (9125 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/churn_monitoring_helper.dart
├─ constants.dart (2914 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/constants.dart
├─ copyright_utils.dart (3478 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/copyright_utils.dart
├─ theme_constants.dart (29904 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/theme_constants.dart
├─ translation_validator.dart (2903 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/translation_validator.dart
📁 widgets/
📁 donate/
├─ animated_donation_header.dart (9488 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/animated_donation_header.dart
├─ badge_preview_dialog.dart (6672 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/badge_preview_dialog.dart
├─ donate_amount_selector.dart (4644 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_amount_selector.dart
├─ donate_badge_grid.dart (4578 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_badge_grid.dart
├─ donate_success_page.dart (8455 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_success_page.dart
├─ floating_continue_button.dart (3696 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/floating_continue_button.dart
├─ add_prayer_modal.dart (11243 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/add_prayer_modal.dart
├─ add_thanksgiving_modal.dart (11533 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/add_thanksgiving_modal.dart
├─ answer_prayer_modal.dart (8344 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/answer_prayer_modal.dart
├─ app_bar_constants.dart (1564 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/app_bar_constants.dart
├─ app_gradient_dialog.dart (2539 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/app_gradient_dialog.dart
├─ backup_configuration_sheet.dart (11585 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/backup_configuration_sheet.dart
├─ backup_settings_content.dart (26360 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/backup_settings_content.dart
├─ badge_image_widget.dart (7167 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/badge_image_widget.dart
├─ bible_book_selector_dialog.dart (7430 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_book_selector_dialog.dart
├─ bible_chapter_grid_selector.dart (5737 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_chapter_grid_selector.dart
├─ bible_reader_action_modal.dart (7168 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_reader_action_modal.dart
├─ bible_reader_drawer.dart (19688 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_reader_drawer.dart
├─ bible_search_bar.dart (5203 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_search_bar.dart
├─ bible_search_overlay.dart (14462 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_search_overlay.dart
├─ bible_verse_grid_selector.dart (5804 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_verse_grid_selector.dart
├─ devocionales_page_drawer.dart (27293 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/devocionales_page_drawer.dart
├─ floating_font_control_buttons.dart (6053 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/floating_font_control_buttons.dart
├─ modern_voice_feature_dialog.dart (3067 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/modern_voice_feature_dialog.dart
├─ offline_manager_widget.dart (8829 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/offline_manager_widget.dart
├─ theme_selector.dart (3060 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/theme_selector.dart
├─ tts_player_widget.dart (9584 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/tts_player_widget.dart
├─ voice_selector_dialog.dart (46325 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/voice_selector_dialog.dart
├─ main.dart (22310 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/main.dart
├─ splash_screen.dart (12967 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/splash_screen.dart
📁 test/
📁 controllers/
├─ tts_audio_controller_test.dart (4370 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/controllers/tts_audio_controller_test.dart
📁 critical_coverage/
├─ audio_controller_user_flows_test.dart (12220 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/audio_controller_user_flows_test.dart
├─ audio_controller_working_test.dart (9713 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/audio_controller_working_test.dart
├─ backup_bloc_working_test.dart (4422 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/backup_bloc_working_test.dart
├─ bible_text_formatter_test.dart (8871 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/bible_text_formatter_test.dart
├─ bible_version_language_download_test.dart (23278 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/bible_version_language_download_test.dart
├─ compression_service_working_test.dart (9417 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/compression_service_working_test.dart
├─ connectivity_service_working_test.dart (13540 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/connectivity_service_working_test.dart
├─ devocional_model_user_flows_test.dart (12342 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_model_user_flows_test.dart
├─ devocional_model_working_test.dart (7437 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_model_working_test.dart
├─ devocional_provider_working_test.dart (9947 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_provider_working_test.dart
├─ devocionales_bloc_test.dart (10203 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocionales_bloc_test.dart
├─ devocionales_tracking_test.dart (8728 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocionales_tracking_test.dart
├─ google_drive_backup_service_working_test.dart (20877 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/google_drive_backup_service_working_test.dart
├─ in_app_review_service_test.dart (7002 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/in_app_review_service_test.dart
├─ localization_service_user_flows_test.dart (7784 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/localization_service_user_flows_test.dart
├─ notification_service_working_test.dart (9169 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/notification_service_working_test.dart
├─ onboarding_bloc_user_flows_test.dart (11711 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/onboarding_bloc_user_flows_test.dart
├─ onboarding_service_test.dart (9467 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/onboarding_service_test.dart
├─ prayer_bloc_working_test.dart (7458 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/prayer_bloc_working_test.dart
├─ prayer_user_flows_test.dart (12687 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/prayer_user_flows_test.dart
├─ remote_badge_service_test.dart (6286 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/remote_badge_service_test.dart
├─ spiritual_stats_model_test.dart (11429 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/spiritual_stats_model_test.dart
├─ spiritual_stats_service_working_test.dart (13475 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/spiritual_stats_service_working_test.dart
├─ thanksgiving_bloc_working_test.dart (6081 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/thanksgiving_bloc_working_test.dart
├─ thanksgiving_user_flows_test.dart (13012 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/thanksgiving_user_flows_test.dart
├─ theme_bloc_user_flows_test.dart (10475 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/theme_bloc_user_flows_test.dart
├─ update_service_test.dart (7427 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/update_service_test.dart
├─ voice_settings_service_test.dart (13567 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/voice_settings_service_test.dart
📁 integration/
├─ churn_monitoring_integration_test.dart (6270 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/churn_monitoring_integration_test.dart
├─ system_navigation_bar_integration_test.dart (7183 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/system_navigation_bar_integration_test.dart
├─ thanksgiving_integration_test.dart (9642 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/thanksgiving_integration_test.dart
├─ tts_di_integration_test.dart (11806 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/tts_di_integration_test.dart
├─ voice_persistence_user_test.dart (7843 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/voice_persistence_user_test.dart
📁 migration/
├─ no_singleton_antipatterns_test.dart (7346 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/migration/no_singleton_antipatterns_test.dart
📁 providers/
├─ localization_provider_test.dart (12620 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/providers/localization_provider_test.dart
📁 services/
├─ churn_prediction_service_test.dart (14759 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/services/churn_prediction_service_test.dart
├─ tts_service_test.dart (3952 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/services/tts_service_test.dart
📁 unit/
📁 android/
├─ android_15_edge_to_edge_test.dart (9318 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/android/android_15_edge_to_edge_test.dart
📁 extensions/
├─ string_extensions_test.dart (2983 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/extensions/string_extensions_test.dart
📁 features/
├─ thanksgiving_user_flow_test.dart (5431 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/features/thanksgiving_user_flow_test.dart
📁 models/
├─ bible_version_test.dart (1830 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/bible_version_test.dart
├─ devocional_model_test.dart (4658 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/devocional_model_test.dart
├─ prayer_model_test.dart (2354 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/prayer_model_test.dart
├─ spiritual_stats_model_test.dart (2414 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/spiritual_stats_model_test.dart
├─ thanksgiving_model_test.dart (3784 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/thanksgiving_model_test.dart
📁 pages/
├─ bible_chapter_navigation_test.dart (6417 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_chapter_navigation_test.dart
├─ bible_consecutive_verse_navigation_test.dart (7236 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_consecutive_verse_navigation_test.dart
├─ bible_reader_enhancements_test.dart (7878 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_enhancements_test.dart
├─ bible_reader_fixes_test.dart (7115 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_fixes_test.dart
├─ bible_reader_navigation_test.dart (5108 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_navigation_test.dart
├─ bible_reader_page_border_theme_test.dart (4353 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_page_border_theme_test.dart
├─ bible_reader_page_test.dart (4342 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_page_test.dart
├─ bible_reader_scroll_precision_test.dart (7254 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_scroll_precision_test.dart
├─ bible_simplified_scroll_test.dart (4225 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_simplified_scroll_test.dart
├─ devocionales_page_share_test.dart (14228 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/devocionales_page_share_test.dart
├─ prayers_page_tabs_test.dart (3223 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/prayers_page_tabs_test.dart
├─ progress_page_tip_test.dart (3395 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/progress_page_tip_test.dart
📁 services/
├─ bible_db_service_test.dart (1257 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_db_service_test.dart
├─ bible_multiword_search_test.dart (6653 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_multiword_search_test.dart
├─ bible_reading_position_service_test.dart (2618 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_reading_position_service_test.dart
├─ localization_service_test.dart (17711 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/localization_service_test.dart
├─ onboarding_service_test.dart (1450 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/onboarding_service_test.dart
├─ service_locator_test.dart (4792 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/service_locator_test.dart
├─ tts_language_initialization_test.dart (4564 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/tts_language_initialization_test.dart
├─ tts_service_behavior_test.dart (16228 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/tts_service_behavior_test.dart
├─ voice_settings_service_critical_test.dart (10209 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/voice_settings_service_critical_test.dart
├─ voice_settings_service_test.dart (8546 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/voice_settings_service_test.dart
📁 translations/
├─ drawer_and_url_test.dart (5253 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/translations/drawer_and_url_test.dart
📁 utils/
├─ bible_reference_parser_test.dart (3929 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_reference_parser_test.dart
├─ bible_text_normalizer_test.dart (2652 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_text_normalizer_test.dart
├─ bible_version_registry_test.dart (3823 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_version_registry_test.dart
├─ constants_validation_test.dart (8882 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/constants_validation_test.dart
├─ dark_mode_textfield_theme_test.dart (5004 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/dark_mode_textfield_theme_test.dart
├─ system_ui_overlay_style_test.dart (5100 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/system_ui_overlay_style_test.dart
├─ theme_outlined_button_border_test.dart (7115 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/theme_outlined_button_border_test.dart
📁 widgets/
├─ bible_chapter_grid_selector_test.dart (12782 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/bible_chapter_grid_selector_test.dart
├─ bible_verse_grid_selector_test.dart (13583 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/bible_verse_grid_selector_test.dart
├─ prayers_page_badges_test.dart (7636 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/prayers_page_badges_test.dart
├─ language_initialization_test.dart (887 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/language_initialization_test.dart
📁 utils/
├─ churn_monitoring_helper_test.dart (11332 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/utils/churn_monitoring_helper_test.dart
📁 widget/
├─ add_thanksgiving_modal_test.dart (6431 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/add_thanksgiving_modal_test.dart
├─ answer_prayer_modal_test.dart (2290 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/answer_prayer_modal_test.dart
├─ main_initialization_test.dart (5826 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/main_initialization_test.dart
├─ tts_player_widget_user_flow_test.dart (15983 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/tts_player_widget_user_flow_test.dart
├─ bible_text_formatter_test.dart (5591 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/bible_text_formatter_test.dart
├─ devocional_reading_logic_test.dart (9173 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/devocional_reading_logic_test.dart
├─ progress_page_overflow_test.dart (5776 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/progress_page_overflow_test.dart
├─ pubspec.yaml (1951 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/pubspec.yaml
📄 ARCHIVOS IMPORTANTES (204 archivos):
========================================
📋 i18n/en.json
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/en.json
📏 31999 bytes
📋 i18n/es.json
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/es.json
📏 34265 bytes
📋 i18n/fr.json
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/fr.json
📏 35998 bytes
📋 i18n/ja.json
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/ja.json
📏 39006 bytes
📋 i18n/pt.json
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/i18n/pt.json
📏 34278 bytes
📋 lib/adapters/http_client_adapter.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/adapters/http_client_adapter.dart
📏 2522 bytes
📋 lib/adapters/storage_adapter.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/adapters/storage_adapter.dart
📏 3862 bytes
📋 lib/blocs/bible_version/bible_version_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_bloc.dart
📏 10608 bytes
📋 lib/blocs/bible_version/bible_version_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_event.dart
📏 3095 bytes
📋 lib/blocs/bible_version/bible_version_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/bible_version/bible_version_state.dart
📏 3233 bytes
📋 lib/blocs/devocionales/devocionales_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_bloc.dart
📏 2007 bytes
📋 lib/blocs/devocionales/devocionales_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_event.dart
📏 366 bytes
📋 lib/blocs/devocionales/devocionales_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/devocionales/devocionales_state.dart
📏 603 bytes
📋 lib/blocs/onboarding/onboarding_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_bloc.dart
📏 36501 bytes
📋 lib/blocs/onboarding/onboarding_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_event.dart
📏 2200 bytes
📋 lib/blocs/onboarding/onboarding_models.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_models.dart
📏 7002 bytes
📋 lib/blocs/onboarding/onboarding_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/onboarding/onboarding_state.dart
📏 3448 bytes
📋 lib/blocs/theme/theme_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_bloc.dart
📏 4668 bytes
📋 lib/blocs/theme/theme_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_event.dart
📏 943 bytes
📋 lib/blocs/theme/theme_repository.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_repository.dart
📏 2537 bytes
📋 lib/blocs/theme/theme_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/theme/theme_state.dart
📏 2771 bytes
📋 lib/blocs/backup_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_bloc.dart
📏 16740 bytes
📋 lib/blocs/backup_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_event.dart
📏 2172 bytes
📋 lib/blocs/backup_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/backup_state.dart
📏 3707 bytes
📋 lib/blocs/prayer_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_bloc.dart
📏 9851 bytes
📋 lib/blocs/prayer_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_event.dart
📏 1104 bytes
📋 lib/blocs/prayer_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/prayer_state.dart
📏 1974 bytes
📋 lib/blocs/thanksgiving_bloc.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_bloc.dart
📏 8048 bytes
📋 lib/blocs/thanksgiving_event.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_event.dart
📏 890 bytes
📋 lib/blocs/thanksgiving_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/blocs/thanksgiving_state.dart
📏 1185 bytes
📋 lib/controllers/audio_controller.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/controllers/audio_controller.dart
📏 22409 bytes
📋 lib/controllers/tts_audio_controller.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/controllers/tts_audio_controller.dart
📏 2771 bytes
📋 lib/debug/debug_settings_section.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/debug_settings_section.dart
📏 4916 bytes
📋 lib/debug/debug_settings_section_stub.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/debug_settings_section_stub.dart
📏 901 bytes
📋 lib/debug/test_badges_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/debug/test_badges_page.dart
📏 6800 bytes
📋 lib/extensions/string_extensions.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/extensions/string_extensions.dart
📏 477 bytes
📋 lib/models/badge_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/badge_model.dart
📏 1932 bytes
📋 lib/models/devocional_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/devocional_model.dart
📏 3567 bytes
📋 lib/models/prayer_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/prayer_model.dart
📏 4418 bytes
📋 lib/models/spiritual_stats_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/spiritual_stats_model.dart
📏 9413 bytes
📋 lib/models/thanksgiving_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/models/thanksgiving_model.dart
📏 1962 bytes
📋 lib/pages/onboarding/onboarding_backup_configuration_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_backup_configuration_page.dart
📏 5798 bytes
📋 lib/pages/onboarding/onboarding_complete_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_complete_page.dart
📏 17604 bytes
📋 lib/pages/onboarding/onboarding_flow.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_flow.dart
📏 11434 bytes
📋 lib/pages/onboarding/onboarding_theme_selection_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_theme_selection_page.dart
📏 12079 bytes
📋 lib/pages/onboarding/onboarding_welcome_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/onboarding/onboarding_welcome_page.dart
📏 4448 bytes
📋 lib/pages/about_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/about_page.dart
📏 7837 bytes
📋 lib/pages/application_language_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/application_language_page.dart
📏 15474 bytes
📋 lib/pages/backup_settings_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/backup_settings_page.dart
📏 30391 bytes
📋 lib/pages/bible_reader_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/bible_reader_page.dart
📏 40770 bytes
📋 lib/pages/bible_versions_manager_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/bible_versions_manager_page.dart
📏 12247 bytes
📋 lib/pages/contact_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/contact_page.dart
📏 12777 bytes
📋 lib/pages/devocionales_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/devocionales_page.dart
📏 49929 bytes
📋 lib/pages/favorites_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/favorites_page.dart
📏 8785 bytes
📋 lib/pages/notification_config_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/notification_config_page.dart
📏 19157 bytes
📋 lib/pages/prayers_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/prayers_page.dart
📏 41475 bytes
📋 lib/pages/progress_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/progress_page.dart
📏 22049 bytes
📋 lib/pages/settings_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/pages/settings_page.dart
📏 25743 bytes
📋 lib/providers/bible_selected_version_provider.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/bible_selected_version_provider.dart
📏 11378 bytes
📋 lib/providers/devocional_provider.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/devocional_provider.dart
📏 31372 bytes
📋 lib/providers/localization_provider.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/providers/localization_provider.dart
📏 2004 bytes
📋 lib/services/tts/bible_text_formatter.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/bible_text_formatter.dart
📏 7949 bytes
📋 lib/services/tts/i_tts_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/i_tts_service.dart
📏 1675 bytes
📋 lib/services/tts/voice_settings_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts/voice_settings_service.dart
📏 24885 bytes
📋 lib/services/churn_prediction_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/churn_prediction_service.dart
📏 17914 bytes
📋 lib/services/compression_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/compression_service.dart
📏 3961 bytes
📋 lib/services/connectivity_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/connectivity_service.dart
📏 2667 bytes
📋 lib/services/devocionales_tracking.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/devocionales_tracking.dart
📏 9613 bytes
📋 lib/services/google_drive_auth_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/google_drive_auth_service.dart
📏 10482 bytes
📋 lib/services/google_drive_backup_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/google_drive_backup_service.dart
📏 28750 bytes
📋 lib/services/in_app_review_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/in_app_review_service.dart
📏 14148 bytes
📋 lib/services/localization_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/localization_service.dart
📏 7800 bytes
📋 lib/services/notification_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/notification_service.dart
📏 32285 bytes
📋 lib/services/onboarding_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/onboarding_service.dart
📏 6082 bytes
📋 lib/services/remote_badge_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/remote_badge_service.dart
📏 6565 bytes
📋 lib/services/service_locator.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/service_locator.dart
📏 6124 bytes
📋 lib/services/spiritual_stats_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/spiritual_stats_service.dart
📏 28179 bytes
📋 lib/services/tts_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/tts_service.dart
📏 19840 bytes
📋 lib/services/update_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/services/update_service.dart
📏 5551 bytes
📋 lib/utils/bubble_constants.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/bubble_constants.dart
📏 14334 bytes
📋 lib/utils/churn_monitoring_helper.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/churn_monitoring_helper.dart
📏 9125 bytes
📋 lib/utils/constants.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/constants.dart
📏 2914 bytes
📋 lib/utils/copyright_utils.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/copyright_utils.dart
📏 3478 bytes
📋 lib/utils/theme_constants.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/theme_constants.dart
📏 29904 bytes
📋 lib/utils/translation_validator.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/utils/translation_validator.dart
📏 2903 bytes
📋 lib/widgets/donate/animated_donation_header.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/animated_donation_header.dart
📏 9488 bytes
📋 lib/widgets/donate/badge_preview_dialog.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/badge_preview_dialog.dart
📏 6672 bytes
📋 lib/widgets/donate/donate_amount_selector.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_amount_selector.dart
📏 4644 bytes
📋 lib/widgets/donate/donate_badge_grid.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_badge_grid.dart
📏 4578 bytes
📋 lib/widgets/donate/donate_success_page.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/donate_success_page.dart
📏 8455 bytes
📋 lib/widgets/donate/floating_continue_button.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/donate/floating_continue_button.dart
📏 3696 bytes
📋 lib/widgets/add_prayer_modal.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/add_prayer_modal.dart
📏 11243 bytes
📋 lib/widgets/add_thanksgiving_modal.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/add_thanksgiving_modal.dart
📏 11533 bytes
📋 lib/widgets/answer_prayer_modal.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/answer_prayer_modal.dart
📏 8344 bytes
📋 lib/widgets/app_bar_constants.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/app_bar_constants.dart
📏 1564 bytes
📋 lib/widgets/app_gradient_dialog.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/app_gradient_dialog.dart
📏 2539 bytes
📋 lib/widgets/backup_configuration_sheet.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/backup_configuration_sheet.dart
📏 11585 bytes
📋 lib/widgets/backup_settings_content.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/backup_settings_content.dart
📏 26360 bytes
📋 lib/widgets/badge_image_widget.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/badge_image_widget.dart
📏 7167 bytes
📋 lib/widgets/bible_book_selector_dialog.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_book_selector_dialog.dart
📏 7430 bytes
📋 lib/widgets/bible_chapter_grid_selector.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_chapter_grid_selector.dart
📏 5737 bytes
📋 lib/widgets/bible_reader_action_modal.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_reader_action_modal.dart
📏 7168 bytes
📋 lib/widgets/bible_reader_drawer.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_reader_drawer.dart
📏 19688 bytes
📋 lib/widgets/bible_search_bar.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_search_bar.dart
📏 5203 bytes
📋 lib/widgets/bible_search_overlay.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_search_overlay.dart
📏 14462 bytes
📋 lib/widgets/bible_verse_grid_selector.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/bible_verse_grid_selector.dart
📏 5804 bytes
📋 lib/widgets/devocionales_page_drawer.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/devocionales_page_drawer.dart
📏 27293 bytes
📋 lib/widgets/floating_font_control_buttons.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/floating_font_control_buttons.dart
📏 6053 bytes
📋 lib/widgets/modern_voice_feature_dialog.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/modern_voice_feature_dialog.dart
📏 3067 bytes
📋 lib/widgets/offline_manager_widget.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/offline_manager_widget.dart
📏 8829 bytes
📋 lib/widgets/theme_selector.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/theme_selector.dart
📏 3060 bytes
📋 lib/widgets/tts_player_widget.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/tts_player_widget.dart
📏 9584 bytes
📋 lib/widgets/voice_selector_dialog.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/widgets/voice_selector_dialog.dart
📏 46325 bytes
📋 lib/main.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/main.dart
📏 22310 bytes
📋 lib/splash_screen.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/lib/splash_screen.dart
📏 12967 bytes
📋 test/controllers/tts_audio_controller_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/controllers/tts_audio_controller_test.dart
📏 4370 bytes
📋 test/critical_coverage/audio_controller_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/audio_controller_user_flows_test.dart
📏 12220 bytes
📋 test/critical_coverage/audio_controller_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/audio_controller_working_test.dart
📏 9713 bytes
📋 test/critical_coverage/backup_bloc_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/backup_bloc_working_test.dart
📏 4422 bytes
📋 test/critical_coverage/bible_text_formatter_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/bible_text_formatter_test.dart
📏 8871 bytes
📋 test/critical_coverage/bible_version_language_download_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/bible_version_language_download_test.dart
📏 23278 bytes
📋 test/critical_coverage/compression_service_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/compression_service_working_test.dart
📏 9417 bytes
📋 test/critical_coverage/connectivity_service_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/connectivity_service_working_test.dart
📏 13540 bytes
📋 test/critical_coverage/devocional_model_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_model_user_flows_test.dart
📏 12342 bytes
📋 test/critical_coverage/devocional_model_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_model_working_test.dart
📏 7437 bytes
📋 test/critical_coverage/devocional_provider_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocional_provider_working_test.dart
📏 9947 bytes
📋 test/critical_coverage/devocionales_bloc_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocionales_bloc_test.dart
📏 10203 bytes
📋 test/critical_coverage/devocionales_tracking_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/devocionales_tracking_test.dart
📏 8728 bytes
📋 test/critical_coverage/google_drive_backup_service_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/google_drive_backup_service_working_test.dart
📏 20877 bytes
📋 test/critical_coverage/in_app_review_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/in_app_review_service_test.dart
📏 7002 bytes
📋 test/critical_coverage/localization_service_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/localization_service_user_flows_test.dart
📏 7784 bytes
📋 test/critical_coverage/notification_service_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/notification_service_working_test.dart
📏 9169 bytes
📋 test/critical_coverage/onboarding_bloc_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/onboarding_bloc_user_flows_test.dart
📏 11711 bytes
📋 test/critical_coverage/onboarding_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/onboarding_service_test.dart
📏 9467 bytes
📋 test/critical_coverage/prayer_bloc_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/prayer_bloc_working_test.dart
📏 7458 bytes
📋 test/critical_coverage/prayer_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/prayer_user_flows_test.dart
📏 12687 bytes
📋 test/critical_coverage/remote_badge_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/remote_badge_service_test.dart
📏 6286 bytes
📋 test/critical_coverage/spiritual_stats_model_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/spiritual_stats_model_test.dart
📏 11429 bytes
📋 test/critical_coverage/spiritual_stats_service_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/spiritual_stats_service_working_test.dart
📏 13475 bytes
📋 test/critical_coverage/thanksgiving_bloc_working_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/thanksgiving_bloc_working_test.dart
📏 6081 bytes
📋 test/critical_coverage/thanksgiving_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/thanksgiving_user_flows_test.dart
📏 13012 bytes
📋 test/critical_coverage/theme_bloc_user_flows_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/theme_bloc_user_flows_test.dart
📏 10475 bytes
📋 test/critical_coverage/update_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/update_service_test.dart
📏 7427 bytes
📋 test/critical_coverage/voice_settings_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/critical_coverage/voice_settings_service_test.dart
📏 13567 bytes
📋 test/integration/churn_monitoring_integration_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/churn_monitoring_integration_test.dart
📏 6270 bytes
📋 test/integration/system_navigation_bar_integration_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/system_navigation_bar_integration_test.dart
📏 7183 bytes
📋 test/integration/thanksgiving_integration_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/thanksgiving_integration_test.dart
📏 9642 bytes
📋 test/integration/tts_di_integration_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/tts_di_integration_test.dart
📏 11806 bytes
📋 test/integration/voice_persistence_user_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/integration/voice_persistence_user_test.dart
📏 7843 bytes
📋 test/migration/no_singleton_antipatterns_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/migration/no_singleton_antipatterns_test.dart
📏 7346 bytes
📋 test/providers/localization_provider_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/providers/localization_provider_test.dart
📏 12620 bytes
📋 test/services/churn_prediction_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/services/churn_prediction_service_test.dart
📏 14759 bytes
📋 test/services/tts_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/services/tts_service_test.dart
📏 3952 bytes
📋 test/unit/android/android_15_edge_to_edge_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/android/android_15_edge_to_edge_test.dart
📏 9318 bytes
📋 test/unit/extensions/string_extensions_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/extensions/string_extensions_test.dart
📏 2983 bytes
📋 test/unit/features/thanksgiving_user_flow_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/features/thanksgiving_user_flow_test.dart
📏 5431 bytes
📋 test/unit/models/bible_version_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/bible_version_test.dart
📏 1830 bytes
📋 test/unit/models/devocional_model_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/devocional_model_test.dart
📏 4658 bytes
📋 test/unit/models/prayer_model_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/prayer_model_test.dart
📏 2354 bytes
📋 test/unit/models/spiritual_stats_model_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/spiritual_stats_model_test.dart
📏 2414 bytes
📋 test/unit/models/thanksgiving_model_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/models/thanksgiving_model_test.dart
📏 3784 bytes
📋 test/unit/pages/bible_chapter_navigation_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_chapter_navigation_test.dart
📏 6417 bytes
📋 test/unit/pages/bible_consecutive_verse_navigation_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_consecutive_verse_navigation_test.dart
📏 7236 bytes
📋 test/unit/pages/bible_reader_enhancements_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_enhancements_test.dart
📏 7878 bytes
📋 test/unit/pages/bible_reader_fixes_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_fixes_test.dart
📏 7115 bytes
📋 test/unit/pages/bible_reader_navigation_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_navigation_test.dart
📏 5108 bytes
📋 test/unit/pages/bible_reader_page_border_theme_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_page_border_theme_test.dart
📏 4353 bytes
📋 test/unit/pages/bible_reader_page_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_page_test.dart
📏 4342 bytes
📋 test/unit/pages/bible_reader_scroll_precision_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_reader_scroll_precision_test.dart
📏 7254 bytes
📋 test/unit/pages/bible_simplified_scroll_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/bible_simplified_scroll_test.dart
📏 4225 bytes
📋 test/unit/pages/devocionales_page_share_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/devocionales_page_share_test.dart
📏 14228 bytes
📋 test/unit/pages/prayers_page_tabs_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/prayers_page_tabs_test.dart
📏 3223 bytes
📋 test/unit/pages/progress_page_tip_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/pages/progress_page_tip_test.dart
📏 3395 bytes
📋 test/unit/services/bible_db_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_db_service_test.dart
📏 1257 bytes
📋 test/unit/services/bible_multiword_search_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_multiword_search_test.dart
📏 6653 bytes
📋 test/unit/services/bible_reading_position_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/bible_reading_position_service_test.dart
📏 2618 bytes
📋 test/unit/services/localization_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/localization_service_test.dart
📏 17711 bytes
📋 test/unit/services/onboarding_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/onboarding_service_test.dart
📏 1450 bytes
📋 test/unit/services/service_locator_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/service_locator_test.dart
📏 4792 bytes
📋 test/unit/services/tts_language_initialization_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/tts_language_initialization_test.dart
📏 4564 bytes
📋 test/unit/services/tts_service_behavior_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/tts_service_behavior_test.dart
📏 16228 bytes
📋 test/unit/services/voice_settings_service_critical_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/voice_settings_service_critical_test.dart
📏 10209 bytes
📋 test/unit/services/voice_settings_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/services/voice_settings_service_test.dart
📏 8546 bytes
📋 test/unit/translations/drawer_and_url_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/translations/drawer_and_url_test.dart
📏 5253 bytes
📋 test/unit/utils/bible_reference_parser_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_reference_parser_test.dart
📏 3929 bytes
📋 test/unit/utils/bible_text_normalizer_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_text_normalizer_test.dart
📏 2652 bytes
📋 test/unit/utils/bible_version_registry_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/bible_version_registry_test.dart
📏 3823 bytes
📋 test/unit/utils/constants_validation_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/constants_validation_test.dart
📏 8882 bytes
📋 test/unit/utils/dark_mode_textfield_theme_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/dark_mode_textfield_theme_test.dart
📏 5004 bytes
📋 test/unit/utils/system_ui_overlay_style_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/system_ui_overlay_style_test.dart
📏 5100 bytes
📋 test/unit/utils/theme_outlined_button_border_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/utils/theme_outlined_button_border_test.dart
📏 7115 bytes
📋 test/unit/widgets/bible_chapter_grid_selector_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/bible_chapter_grid_selector_test.dart
📏 12782 bytes
📋 test/unit/widgets/bible_verse_grid_selector_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/bible_verse_grid_selector_test.dart
📏 13583 bytes
📋 test/unit/widgets/prayers_page_badges_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/widgets/prayers_page_badges_test.dart
📏 7636 bytes
📋 test/unit/language_initialization_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/unit/language_initialization_test.dart
📏 887 bytes
📋 test/utils/churn_monitoring_helper_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/utils/churn_monitoring_helper_test.dart
📏 11332 bytes
📋 test/widget/add_thanksgiving_modal_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/add_thanksgiving_modal_test.dart
📏 6431 bytes
📋 test/widget/answer_prayer_modal_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/answer_prayer_modal_test.dart
📏 2290 bytes
📋 test/widget/main_initialization_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/main_initialization_test.dart
📏 5826 bytes
📋 test/widget/tts_player_widget_user_flow_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/widget/tts_player_widget_user_flow_test.dart
📏 15983 bytes
📋 test/bible_text_formatter_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/bible_text_formatter_test.dart
📏 5591 bytes
📋 test/devocional_reading_logic_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/devocional_reading_logic_test.dart
📏 9173 bytes
📋 test/progress_page_overflow_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/test/progress_page_overflow_test.dart
📏 5776 bytes
📋 pubspec.yaml
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/pubspec.yaml
📏 1951 bytes
MÓDULO CORE: bible_reader_core
CARPETAS ANALIZADAS: lib, test + pubspec.yaml
================================================================================
📁 ESTRUCTURA DEL MÓDULO:
========================================
📁 lib/
📁 src/
📁 exceptions/
├─ bible_version_error_code.dart (1817 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/exceptions/bible_version_error_code.dart
├─ bible_version_exceptions.dart (5765 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/exceptions/bible_version_exceptions.dart
📁 interfaces/
├─ bible_version_storage.dart (2708 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/interfaces/bible_version_storage.dart
├─ http_client.dart (2851 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/interfaces/http_client.dart
📁 models/
├─ bible_version_model.dart (10457 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/models/bible_version_model.dart
📁 repositories/
├─ bible_version_repository.dart (25099 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/repositories/bible_version_repository.dart
├─ bible_db_service.dart (10354 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_db_service.dart
├─ bible_preferences_service.dart (1939 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_preferences_service.dart
├─ bible_reader_controller.dart (17297 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_controller.dart
├─ bible_reader_service.dart (7222 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_service.dart
├─ bible_reader_state.dart (3201 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_state.dart
├─ bible_reading_position_service.dart (2224 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reading_position_service.dart
├─ bible_reference_parser.dart (2100 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reference_parser.dart
├─ bible_text_normalizer.dart (480 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_text_normalizer.dart
├─ bible_verse_formatter.dart (1624 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_verse_formatter.dart
├─ bible_version.dart (398 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_version.dart
├─ bible_version_registry.dart (3057 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_version_registry.dart
├─ bible_reader_core.dart (809 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/bible_reader_core.dart
📁 test/
📁 repositories/
├─ bible_version_repository_test.dart (29002 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/repositories/bible_version_repository_test.dart
├─ bible_preferences_service_test.dart (3905 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_preferences_service_test.dart
├─ bible_reader_controller_test.dart (14700 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_controller_test.dart
├─ bible_reader_core_test.dart (219 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_core_test.dart
├─ bible_reader_service_test.dart (14108 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_service_test.dart
├─ bible_reference_parser_test.dart (3929 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reference_parser_test.dart
├─ pubspec.yaml (406 bytes)
📄 RAW: https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/pubspec.yaml
📄 ARCHIVOS IMPORTANTES (25 archivos):
========================================
📋 bible_reader_core/lib/src/exceptions/bible_version_error_code.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/exceptions/bible_version_error_code.dart
📏 1817 bytes
📋 bible_reader_core/lib/src/exceptions/bible_version_exceptions.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/exceptions/bible_version_exceptions.dart
📏 5765 bytes
📋 bible_reader_core/lib/src/interfaces/bible_version_storage.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/interfaces/bible_version_storage.dart
📏 2708 bytes
📋 bible_reader_core/lib/src/interfaces/http_client.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/interfaces/http_client.dart
📏 2851 bytes
📋 bible_reader_core/lib/src/models/bible_version_model.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/models/bible_version_model.dart
📏 10457 bytes
📋 bible_reader_core/lib/src/repositories/bible_version_repository.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/repositories/bible_version_repository.dart
📏 25099 bytes
📋 bible_reader_core/lib/src/bible_db_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_db_service.dart
📏 10354 bytes
📋 bible_reader_core/lib/src/bible_preferences_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_preferences_service.dart
📏 1939 bytes
📋 bible_reader_core/lib/src/bible_reader_controller.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_controller.dart
📏 17297 bytes
📋 bible_reader_core/lib/src/bible_reader_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_service.dart
📏 7222 bytes
📋 bible_reader_core/lib/src/bible_reader_state.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reader_state.dart
📏 3201 bytes
📋 bible_reader_core/lib/src/bible_reading_position_service.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reading_position_service.dart
📏 2224 bytes
📋 bible_reader_core/lib/src/bible_reference_parser.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_reference_parser.dart
📏 2100 bytes
📋 bible_reader_core/lib/src/bible_text_normalizer.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_text_normalizer.dart
📏 480 bytes
📋 bible_reader_core/lib/src/bible_verse_formatter.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_verse_formatter.dart
📏 1624 bytes
📋 bible_reader_core/lib/src/bible_version.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_version.dart
📏 398 bytes
📋 bible_reader_core/lib/src/bible_version_registry.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/src/bible_version_registry.dart
📏 3057 bytes
📋 bible_reader_core/lib/bible_reader_core.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/lib/bible_reader_core.dart
📏 809 bytes
📋 bible_reader_core/test/repositories/bible_version_repository_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/repositories/bible_version_repository_test.dart
📏 29002 bytes
📋 bible_reader_core/test/bible_preferences_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_preferences_service_test.dart
📏 3905 bytes
📋 bible_reader_core/test/bible_reader_controller_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_controller_test.dart
📏 14700 bytes
📋 bible_reader_core/test/bible_reader_core_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_core_test.dart
📏 219 bytes
📋 bible_reader_core/test/bible_reader_service_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reader_service_test.dart
📏 14108 bytes
📋 bible_reader_core/test/bible_reference_parser_test.dart
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/test/bible_reference_parser_test.dart
📏 3929 bytes
📋 bible_reader_core/pubspec.yaml
🔗 https://raw.githubusercontent.com/develop4God/Devocional_nuevo/copilot/implement-user-churn-prediction/bible_reader_core/pubspec.yaml
📏 406 bytes
CONTENIDO DE: pubspec.yaml (PROYECTO PRINCIPAL)
==================================================
# pubspec.yaml
name: devocional_nuevo
description: A new Flutter project.
publish_to: 'none'
version: 1.4.1+62
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
package_info_plus: ^8.0.2
flutter_native_splash: ^2.3.1
url_launcher: ^6.3.1
http: ^1.1.0
shared_preferences: ^2.5.3
screenshot: ^3.0.0
share_plus: ^11.0.0
auto_size_text: ^3.0.0
path_provider: ^2.1.5
provider: ^6.0.0
flutter_bloc: ^9.1.1
equatable: ^2.0.5
intl: 0.20.2
cached_network_image: ^3.3.0
in_app_update: ^4.2.3
in_app_review: ^2.0.8
google_fonts: ^6.3.0
lottie: ^3.1.0
sqflite: ^2.3.0
path: ^1.9.0
scrollable_positioned_list: ^0.3.8
flutter_localizations:
sdk: flutter
# Push Notifications
flutter_local_notifications: ^19.3.0
timezone: ^0.10.1
permission_handler: ^12.0.1
flutter_timezone: ^4.1.1 #para obtener la zona horaria del dispositivo real
# Firebase dependencies
firebase_core: 3.6.0
firebase_messaging: 15.1.3
firebase_auth: 5.3.1
cloud_firestore: 5.4.3
# Audio dependencies
flutter_tts: ^4.1.0
synchronized: ^3.1.0+1 # For thread safety and mutex protection
# Google Drive Backup dependencies
googleapis: ^13.2.0
google_sign_in: ^6.2.1
extension_google_sign_in_as_googleapis_auth: ^2.0.12
connectivity_plus: ^6.0.5
archive: ^4.0.0
# Modularización del proyecto
bible_reader_core:
path: bible_reader_core
yaml: ^3.1.3
logger: ^2.6.2
firebase_in_app_messaging: ^0.8.0+8
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter_lints: ^6.0.0
mocktail: ^1.0.0
bloc_test: ^10.0.0
coverage: ^1.7.2
mockito: ^5.4.4
build_runner: ^2.4.12
path_provider_platform_interface: any
test: ^1.25.7
flutter:
uses-material-design: true
assets:
- i18n/
- assets/images/splash_background.png
- assets/icons/
- assets/badges/
- assets/lottie/
- assets/biblia/
🔍 ANÁLISIS DE PULL REQUEST #133
============================================================
📋 INFORMACIÓN GENERAL:
• Título: Implement user churn prediction service with production-ready controls, performance optimization, and comprehensive safeguards
• Estado: open (Open/Closed)
• Autor: Copilot
• Creado: 2025-12-08 05:12:09
• Rama origen: copilot/implement-user-churn-prediction
• Rama destino: main
📝 DESCRIPCIÓN:
## PR #133 - All Blocking Issues Resolved ✅
This commit addresses all 4 critical blocking issues identified in PR feedback.
### Issue #1: Update 9 Obsolete Tests ✅
**Fixed Tests:**
- `test/services/churn_prediction_service_test.dart` (3 tests updated)
- `test/utils/churn_monitoring_helper_test.dart` (complete rewrite + 4 rate limiting tests added)
- `test/integration/churn_monitoring_integration_test.dart` (1 test updated)
**Changes:**
- Updated assertions to expect `ChurnRiskLevel.unknown` for new users and error cases
- Changed error handling to return `unknown` instead of `low`
- Fixed tests to work with new GAP-3 architecture (helper-based notifications)
- Added SharedPreferences mocking in all tests
**Result:** All 34 tests passing ✅
---
### Issue #2: Persist Last Check Timestamp ✅
**File:** `lib/main.dart`
**Changes:**
- Removed in-memory `DateTime? _lastChurnCheck` field
- Implemented SharedPreferences persistence with key `'churn_last_check_timestamp'`
- Load timestamp on each check
- Save timestamp after successful check
- Parse ISO 8601 string format for storage
**Benefits:**
- Timestamp persists across app restarts
- Prevents duplicate checks after app restart
- Adds logging for skipped checks with hours since last check
**Code:**
```dart
// Load from SharedPreferences
final lastCheckString = prefs.getString('churn_last_check_timestamp');
DateTime? lastCheck = lastCheckString != null ? DateTime.parse(lastCheckString) : null;
// Save after successful check
await prefs.setString('churn_last_check_timestamp', now.toIso8601String());
```
---
### Issue #3: Add Rate Limiting Tests ✅
**File:** `test/utils/churn_monitoring_helper_test.dart`
**New Test Group:** `ChurnMonitoringHelper - Rate Limiting`
**4 New Tests:**
1. **enforces 2 notifications per week** - Verifies max 2 notifications in 7-day window
2. **allows notification after 7-day window expires** - Tests sliding window behavior
3. **cleans up old notification history** - Validates automatic cleanup
4. **handles empty notification history** - Edge case for new users
**Coverage:**
- SharedPreferences key: `'churn_notifications_sent'`
- Sliding 7-day window enforcement
- Automatic history cleanup
- `getNotificationHistoryCount()` helper method
**Result:** All rate limiting tests passing ✅
---
### Issue #4: Normalize All UTC Timestamps ✅
**Files Modified:**
- `lib/services/churn_prediction_service.dart`
- `lib/utils/churn_monitoring_helper.dart`
- `lib/main.dart`
**Locations Fixed (6 total):**
1. **Cache timestamp comparison**: `DateTime.now().toUtc().difference(...)`
2. **Cache timestamp storage**: `_cacheTimestamp = DateTime.now().toUtc()`
3. **Error case calculatedAt**: `calculatedAt: DateTime.now().toUtc()`
4. **Notification ID (service)**: `id: DateTime.now().toUtc().millisecondsSinceEpoch`
5. **Notification ID (helper)**: `id: DateTime.now().toUtc().millisecondsSinceEpoch`
6. **Last check timestamp (main)**: `final now = DateTime.now().toUtc()`
**Benefits:**
- Consistent timezone handling across the app
- Fixes bugs for traveling users
- Accurate 7-day sliding windows regardless of timezone
- Cache invalidation works correctly globally
---
### Complete Test Suite Results
**Churn Prediction Tests:** 34/34 passing ✅
- **Service Tests:** 15 tests
- **Helper Tests:** 14 tests (including 4 rate limiting)
- **Integration Tests:** 5 tests
**Breakdown:**
- Risk calculation tests: 6/6 ✅
- Notification tests: 3/3 ✅
- Daily check tests: 3/3 ✅
- Engagement summary: 2/2 ✅
- Error handling: 4/4 ✅
- Edge cases: 2/2 ✅
- Rate limiting: 4/4 ✅
- Integration workflows: 5/5 ✅
- Manual notification: 3/3 ✅
- Risk check: 2/2 ✅
---
### Code Quality
✅ **flutter analyze --fatal-infos**: Clean (only expected deprecation warning)
✅ **dart format**: All files formatted
✅ **All tests passing**: 34/34
✅ **No warnings or errors**: Production-ready
---
### Summary of Changes
**New/Modified Files:**
- `lib/main.dart` - SharedPreferences persistence + UTC normalization
- `lib/services/churn_prediction_service.dart` - UTC normalization (3 locations)
- `lib/utils/churn_monitoring_helper.dart` - UTC normalization (1 location)
- `test/services/churn_prediction_service_test.dart` - Updated for `unknown` risk level + SharedPreferences mock
- `test/utils/churn_monitoring_helper_test.dart` - Complete rewrite + 4 rate limiting tests
- `test/integration/churn_monitoring_integration_test.dart` - Updated for new architecture
**Lines Changed:**
- Production code: ~50 lines
- Test code: ~300 lines (including new rate limiting tests)
**Test Coverage Increase:**
- Added 4 critical rate limiting tests
- Fixed 9 obsolete tests
- Total: 34 passing tests with comprehensive coverage
---
### Ready for Production Deployment 🚀
All 4 blocking issues resolved:
- [x] Issue #1: 9 obsolete tests updated
- [x] Issue #2: Last check timestamp persisted
- [x] Issue #3: Rate limiting tests added (4 new tests)
- [x] Issue #4: All UTC timestamps normalized (6 locations)
**Next Steps:**
1. Code review approval
2. QA testing on device
3. Merge to main
4. Deploy to production
<!-- START COPILOT CODING AGENT SUFFIX -->
<!-- START COPILOT ORIGINAL PROMPT -->
<details>
<summary>Original prompt</summary>
> copilot new feature: implement Detection" / "User Churn Prediction" base in our actual stats. Use new notificaciones locales, utiliza el paquete flutter_local_notifications, junto con la gestión de zona horaria (timezone y flutter_timezone) y permisos (permission_handler).
> Service DI not singleton. Add testing and static validations no warning or errors
</details>
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
📊 ESTADÍSTICAS:
• Commits: 10
• Archivos modificados: 16
• Adiciones: 2419 líneas
• Eliminaciones: 20 líneas
• Archivos cambiados: 16
🔄 COMMITS:
1. [be7c42fa] Initial plan
👤 copilot-swe-agent[bot] - 2025-12-08 05:12
2. [d2d7948a] Initial plan for user churn prediction feature
👤 copilot-swe-agent[bot] - 2025-12-08 05:15
3. [1916aa48] Add churn prediction service with comprehensive testing
👤 copilot-swe-agent[bot] - 2025-12-08 05:18
4. [d34aef39] Add integration helper and complete churn prediction feature
👤 copilot-swe-agent[bot] - 2025-12-08 05:21
5. [c5e35c36] Address code review feedback: fix DI and simplify reading frequency calculation
👤 copilot-swe-agent[bot] - 2025-12-08 05:27
6. [885de519] Final refinements: remove Flutter import, add named constants, consistent logging
👤 copilot-swe-agent[bot] - 2025-12-08 05:31
7. [4a5c203d] Address PR feedback: integrate churn service in main app, add daily scheduling, and implement i18n for notifications
👤 copilot-swe-agent[bot] - 2025-12-08 06:08
8. [864d5f05] Add GAP-4: comprehensive integration test coverage for churn monitoring
👤 copilot-swe-agent[bot] - 2025-12-08 06:17
9. [9cdcb0da] Implement all 7 gaps: user toggle, caching, edge cases, analytics, UTC, feature flag, rate limiting
👤 copilot-swe-agent[bot] - 2025-12-08 14:59
10. [ed655623] Fix all 4 blocking issues: update tests, persist timestamp, add rate limit tests, normalize UTC
👤 copilot-swe-agent[bot] - 2025-12-08 15:29
📁 ARCHIVOS MODIFICADOS:
📝 README.md (+17/-0)
📄 DIFF: 31 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/README.md
✅ docs/CHURN_PREDICTION.md (+338/-0)
📄 DIFF: 339 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/docs%2FCHURN_PREDICTION.md
📝 i18n/en.json (+12/-1)
📄 DIFF: 27 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fen.json
📝 i18n/es.json (+12/-1)
📄 DIFF: 27 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fes.json
📝 i18n/fr.json (+12/-1)
📄 DIFF: 27 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Ffr.json
📝 i18n/ja.json (+12/-1)
📄 DIFF: 27 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fja.json
📝 i18n/pt.json (+12/-1)
📄 DIFF: 27 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fpt.json
📝 lib/main.dart (+93/-1)
📄 DIFF: 135 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fmain.dart
📝 lib/pages/settings_page.dart (+94/-0)
📄 DIFF: 122 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fpages%2Fsettings_page.dart
✅ lib/services/churn_prediction_service.dart (+570/-0)
📄 DIFF: 571 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fservices%2Fchurn_prediction_service.dart
📝 lib/services/service_locator.dart (+35/-0)
📄 DIFF: 53 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fservices%2Fservice_locator.dart
✅ lib/utils/churn_monitoring_helper.dart (+281/-0)
📄 DIFF: 282 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Futils%2Fchurn_monitoring_helper.dart
📝 pubspec.lock (+14/-14)
📄 DIFF: 79 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/pubspec.lock
✅ test/integration/churn_monitoring_integration_test.dart (+171/-0)
📄 DIFF: 172 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Fintegration%2Fchurn_monitoring_integration_test.dart
✅ test/services/churn_prediction_service_test.dart (+404/-0)
📄 DIFF: 405 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Fservices%2Fchurn_prediction_service_test.dart
✅ test/utils/churn_monitoring_helper_test.dart (+342/-0)
📄 DIFF: 343 líneas de cambio
🔗 RAW: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Futils%2Fchurn_monitoring_helper_test.dart
DIFFS COMPLETOS - PR #133
==================================================
📄 ARCHIVO: README.md
Estado: modified (+17/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/README.md
DIFF:
----------------------------------------
@@ -24,6 +24,7 @@ Multilingual mobile application for reading daily devotionals with advanced audi
- **🔊 Audio TTS**: Text-to-speech reading of devotionals
- **⭐ Favorites**: Save your favorite devotionals
- **📊 Spiritual Tracking**: Reading statistics and progress
+- **🧘 Churn Prediction**: Automatic engagement monitoring with smart re-engagement notifications
- **🙏 Prayer Management**: Personal prayer tracking
- **📴 Offline Mode**: Access without internet connection
- **🔔 Notifications**: Customizable reminders
@@ -33,6 +34,22 @@ Multilingual mobile application for reading daily devotionals with advanced audi
- **⭐ Smart Review System**: Requests reviews at optimal moments
- **📱 Android 15 Support**: Compatible with edge-to-edge display and modern APIs
+## 🧘 Churn Prediction
+
+Automatic user engagement monitoring with smart re-engagement notifications.
+
+**Features:**
+- Multi-factor risk analysis (activity, streaks, reading patterns)
+- Configurable notifications (Settings → Notifications → Re-engagement Reminders)
+- Multi-language support (5 languages)
+- Privacy-focused (all data stored locally)
+- Performance optimized with 5-minute caching
+
+**How it works:**
+The system analyzes your reading patterns and engagement metrics to identify when you might need a gentle reminder to return. Notifications are sent only when needed, respecting your preferences.
+
+See [docs/CHURN_PREDICTION.md](docs/CHURN_PREDICTION.md) for technical details.
+
### 🛠️ Technologies
- **Flutter 3.32.8**: Main framework
----------------------------------------
📄 ARCHIVO: docs/CHURN_PREDICTION.md
Estado: added (+338/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/docs%2FCHURN_PREDICTION.md
DIFF:
----------------------------------------
@@ -0,0 +1,338 @@
+# User Churn Detection & Prediction Feature
+
+## Overview
+
+The User Churn Prediction feature analyzes user engagement patterns to identify users at risk of churning (stopping app usage) and automatically sends targeted re-engagement notifications.
+
+## Features
+
+### 1. Churn Risk Analysis
+- **Multi-factor Algorithm**: Analyzes user behavior based on:
+ - Days since last activity
+ - Current streak vs. longest streak
+ - Reading frequency patterns
+ - Overall engagement metrics
+
+- **Risk Levels**:
+ - **Low**: User is actively engaged
+ - **Medium**: User shows signs of decreased engagement
+ - **High**: User is at high risk of churning
+
+### 2. Intelligent Notifications
+- **Risk-based Messaging**: Different notification content for each risk level
+- **Timezone-aware**: Uses existing timezone infrastructure (`flutter_timezone`)
+- **Permission-aware**: Respects user notification preferences
+- **Local Notifications**: Leverages `flutter_local_notifications` package
+
+### 3. Dependency Injection
+- **Non-Singleton Pattern**: Service is registered as factory in `ServiceLocator`
+- **Testable**: Easy to mock and test
+- **No Shared State**: Each instance is independent
+
+## Architecture
+
+```
+ChurnPredictionService (Factory)
+├── Dependencies
+│ ├── SpiritualStatsService
+│ └── NotificationService
+├── Risk Calculation
+│ ├── Days since last activity (40% weight)
+│ ├── Streak decline (30% weight)
+│ ├── Reading frequency (20% weight)
+│ └── Zero current streak (10% weight)
+└── Notification Logic
+ ├── Risk-based decisions
+ ├── User preference checks
+ └── Targeted messaging
+```
+
+## Usage
+
+### Basic Usage
+
+```dart
+import 'package:devocional_nuevo/services/service_locator.dart';
+import 'package:devocional_nuevo/utils/churn_monitoring_helper.dart';
+
+// Perform daily churn check (call from background task or app startup)
+await ChurnMonitoringHelper.performDailyCheck();
+```
+
+### Advanced Usage
+
+```dart
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
+import 'package:devocional_nuevo/services/service_locator.dart';
+
+// Get service instance
+final churnService = getService<ChurnPredictionService>();
+
+// 1. Get current risk prediction
+final prediction = await churnService.predictChurnRisk();
+print('Risk Level: ${prediction.riskLevel}');
+print('Risk Score: ${prediction.riskScore}');
+print('Days Inactive: ${prediction.daysSinceLastActivity}');
+print('Reason: ${prediction.reason}');
+
+// 2. Send manual notification if needed
+if (prediction.shouldSendNotification) {
+ await churnService.sendChurnPreventionNotification(prediction);
+}
+
+// 3. Get engagement summary
+final summary = await churnService.getEngagementSummary();
+print('Total Readings: ${summary['total_readings']}');
+print('Current Streak: ${summary['current_streak']}');
+print('Engagement Status: ${summary['engagement_status']}');
+```
+
+### Helper Methods
+
+The `ChurnMonitoringHelper` class provides convenient methods:
+
+```dart
+// Check if user is at risk
+final riskLevel = await ChurnMonitoringHelper.checkUserRisk();
+
+// Get engagement summary
+final summary = await ChurnMonitoringHelper.getEngagementSummary();
+
+// Manually trigger notification
+await ChurnMonitoringHelper.sendChurnPreventionNotification();
+```
+
+## Configuration
+
+### Risk Thresholds
+
+The service uses the following default thresholds (configurable via constants):
+
+```dart
+_inactiveDaysThresholdHigh = 7; // 1 week of inactivity = HIGH risk
+_inactiveDaysThresholdMedium = 3; // 3 days of inactivity = MEDIUM risk
+_minReadingsForPrediction = 3; // Minimum readings to analyze patterns
+```
+
+### Notification Content
+
+| Risk Level | Title | Body |
+|------------|-------|------|
+| High | ¡Te extrañamos! 🙏 | Han pasado X días. Vuelve a conectarte con tu fe. |
+| Medium | Tu devocional te está esperando 📖 | No pierdas tu racha. Lee el devocional de hoy. |
+| Low | ¡Continúa tu racha! 🔥 | ¡Sigue así! Tu dedicación es inspiradora. |
+
+## Integration
+
+### 1. Service Registration
+
+The service is automatically registered in `setupServiceLocator()`:
+
+```dart
+// lib/services/service_locator.dart
+locator.registerFactory<ChurnPredictionService>(
+ () => ChurnPredictionService(
+ statsService: SpiritualStatsService(),
+ notificationService: NotificationService(),
+ ),
+);
+```
+
+### 2. Background Tasks
+
+To integrate with background tasks or scheduled jobs:
+
+```dart
+// Example: Daily background task
+void scheduleDailyChurnCheck() {
+ // Use your preferred background task scheduler
+ // (e.g., workmanager, flutter_background_service)
+
+ Workmanager().registerPeriodicTask(
+ "churn-check",
+ "churnCheck",
+ frequency: Duration(hours: 24),
+ constraints: Constraints(
+ networkType: NetworkType.not_required,
+ ),
+ );
+}
+
+void callbackDispatcher() {
+ Workmanager().executeTask((task, inputData) async {
+ await ChurnMonitoringHelper.performDailyCheck();
+ return Future.value(true);
+ });
+}
+```
+
+### 3. App Startup Integration
+
+```dart
+// In your main.dart or app initialization
+void initializeChurnMonitoring() async {
+ // Perform initial check on app startup
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // ... other initialization ...
+
+ // Check churn risk on app start
+ try {
+ await ChurnMonitoringHelper.performDailyCheck();
+ } catch (e) {
+ debugPrint('Churn check failed: $e');
+ }
+}
+```
+
+## Testing
+
+The feature includes comprehensive test coverage:
+
+### Test Statistics
+- **Total Tests**: 24
+- **Service Tests**: 15
+- **Integration Helper Tests**: 9
+- **Coverage**: Risk calculation, notifications, error handling, edge cases
+
+### Running Tests
+
+```bash
+# Run churn prediction service tests
+flutter test test/services/churn_prediction_service_test.dart
+
+# Run helper tests
+flutter test test/utils/churn_monitoring_helper_test.dart
+
+# Run all tests
+flutter test
+```
+
+### Test Examples
+
+```dart
+// Example: Testing risk prediction
+test('predicts HIGH risk for inactive user', () async {
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 0,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(Duration(days: 8)),
+ );
+
+ final prediction = await churnService.predictChurnRisk();
+
+ expect(prediction.riskLevel, ChurnRiskLevel.high);
+ expect(prediction.shouldSendNotification, true);
+});
+```
+
+## Algorithm Details
+
+### Risk Score Calculation
+
+The risk score (0.0 to 1.0) is calculated using weighted factors:
+
+1. **Inactivity (40% weight)**
+ - ≥7 days inactive: +0.4
+ - ≥3 days inactive: +0.2
+
+2. **Streak Decline (30% weight)**
+ - Score = (1 - current_streak / longest_streak) × 0.3
+
+3. **Reading Frequency (20% weight)**
+ - <3 total readings: +0.2
+ - Reading rate <0.3/day: +0.2
+
+4. **Zero Current Streak (10% weight)**
+ - Current streak = 0 (but has history): +0.1
+
+### Decision Logic
+
+```
+if (riskScore ≥ 0.6 OR daysInactive ≥ 7):
+ riskLevel = HIGH
+ shouldSendNotification = true
+
+elif (riskScore ≥ 0.3 OR daysInactive ≥ 3):
+ riskLevel = MEDIUM
+ shouldSendNotification = true (if ≥3 days inactive)
+
+else:
+ riskLevel = LOW
+ shouldSendNotification = false
+```
+
+## Dependencies
+
+The feature uses existing infrastructure:
+
+- `flutter_local_notifications`: ^19.3.0 - Local notification display
+- `timezone`: ^0.10.1 - Timezone calculations
+- `permission_handler`: ^12.0.1 - Permission management
+- `flutter_timezone`: ^4.1.1 - Device timezone detection
+
+## Best Practices
+
+1. **Call Daily**: Perform churn checks once per day via background task
+2. **Respect Preferences**: Always check user notification preferences
+3. **Monitor Performance**: Track notification open rates and engagement
+4. **Adjust Thresholds**: Fine-tune risk thresholds based on your user base
+5. **Test Thoroughly**: Use provided tests as examples for custom scenarios
+
+## Troubleshooting
+
+### Notifications Not Sending
+
+1. Check notification permissions:
+ ```dart
+ final enabled = await notificationService.areNotificationsEnabled();
+ ```
+
+2. Verify user has activity history:
+ ```dart
+ final stats = await statsService.getStats();
+ print('Total readings: ${stats.totalDevocionalesRead}');
+ ```
+
+3. Check risk prediction:
+ ```dart
+ final prediction = await churnService.predictChurnRisk();
+ print('Should send: ${prediction.shouldSendNotification}');
+ print('Reason: ${prediction.reason}');
+ ```
+
+### High False Positives
+
+If too many users are marked as high risk:
+- Increase `_inactiveDaysThresholdHigh` (default: 7)
+- Adjust risk score weights
+- Add additional engagement signals
+
+### Low Detection Rate
+
+If at-risk users aren't being detected:
+- Decrease `_inactiveDaysThresholdMedium` (default: 3)
+- Lower the medium risk score threshold (default: 0.3)
+- Add more sensitive engagement metrics
+
+## Future Enhancements
+
+Potential improvements:
+- Machine learning-based predictions
+- A/B testing for notification content
+- Time-of-day optimization
+- Personalized re-engagement strategies
+- Integration with Firebase Analytics
+- Cohort analysis
+
+## License
+
+This feature is part of the Devocional Nuevo app and follows the same license terms.
+
+## Support
+
+For questions or issues:
+1. Check the test files for usage examples
+2. Review the inline documentation
+3. Create an issue in the repository
----------------------------------------
📄 ARCHIVO: i18n/en.json
Estado: modified (+12/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fen.json
DIFF:
----------------------------------------
@@ -151,7 +151,10 @@
"paypal_no_app_error": "Could not open PayPal. Make sure you have a web browser or PayPal app installed.",
"backup_option": "Backup",
"backup_subtitle": "Backup and restore your data securely",
- "voice_sample_text": "Tap to listen to an audio sample. Then save your preference."
+ "voice_sample_text": "Tap to listen to an audio sample. Then save your preference.",
+ "churn_notifications_enabled": "Re-engagement Reminders",
+ "churn_notifications_subtitle": "Receive notifications when you've been away",
+ "churn_notifications_disabled": "You won't receive inactivity reminders"
},
"notifications_config_page": {
"notifications_config": "Notifications Config",
@@ -658,5 +661,13 @@
"no_thanksgivings_description": "Create your first thanksgiving to express your gratitude to God.",
"days_old_single": "({days} day)",
"days_old_plural": "({days} days)"
+ },
+ "churn_notification": {
+ "high_title": "We miss you! 🙏",
+ "high_body": "{days} days have passed. Come back and connect with your faith.",
+ "medium_title": "Your devotional is waiting 📖",
+ "medium_body": "Don't lose your streak. Read today's devotional.",
+ "low_title": "Keep it up! 🔥",
+ "low_body": "Your dedication is inspiring!"
}
}
\ No newline at end of file
----------------------------------------
📄 ARCHIVO: i18n/es.json
Estado: modified (+12/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fes.json
DIFF:
----------------------------------------
@@ -150,7 +150,10 @@
"paypal_no_app_error": "No se pudo abrir PayPal. Asegúrate de tener un navegador web o la app de PayPal instalada.",
"backup_option": "Copia de seguridad",
"backup_subtitle": "Respalda y restaura tus datos de forma segura",
- "voice_sample_text": "Toca para escuchar una muestra de audio. Luego guarde su preferencia."
+ "voice_sample_text": "Toca para escuchar una muestra de audio. Luego guarde su preferencia.",
+ "churn_notifications_enabled": "Recordatorios de Re-enganche",
+ "churn_notifications_subtitle": "Recibe notificaciones cuando has estado ausente",
+ "churn_notifications_disabled": "No recibirás recordatorios de inactividad"
},
"notifications_config_page": {
"notifications_config": "Configuración de notificaciones",
@@ -657,5 +660,13 @@
"no_thanksgivings_description": "Crea tu primer agradecimiento para expresar tu gratitud a Dios.",
"days_old_single": "({days} día)",
"days_old_plural": "({days} días)"
+ },
+ "churn_notification": {
+ "high_title": "¡Te extrañamos! 🙏",
+ "high_body": "Han pasado {days} días. Vuelve a conectarte con tu fe.",
+ "medium_title": "Tu devocional te está esperando 📖",
+ "medium_body": "No pierdas tu racha. Lee el devocional de hoy.",
+ "low_title": "¡Continúa tu racha! 🔥",
+ "low_body": "¡Sigue así! Tu dedicación es inspiradora."
}
}
\ No newline at end of file
----------------------------------------
📄 ARCHIVO: i18n/fr.json
Estado: modified (+12/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Ffr.json
DIFF:
----------------------------------------
@@ -151,7 +151,10 @@
"paypal_no_app_error": "Impossible d'ouvrir PayPal. Assurez-vous d'avoir un navigateur web ou l'application PayPal installée.",
"backup_option": "Sauvegarde",
"backup_subtitle": "Sauvegardez et restaurez vos données en toute sécurité",
- "voice_sample_text": "Appuyez pour écouter un échantillon audio. Ensuite, enregistrez votre préférence."
+ "voice_sample_text": "Appuyez pour écouter un échantillon audio. Ensuite, enregistrez votre préférence.",
+ "churn_notifications_enabled": "Rappels de Ré-engagement",
+ "churn_notifications_subtitle": "Recevez des notifications lorsque vous êtes absent",
+ "churn_notifications_disabled": "Vous ne recevrez pas de rappels d'inactivité"
},
"notifications_config_page": {
"notifications_config": "Config. des Notifications",
@@ -658,5 +661,13 @@
"no_thanksgivings_description": "Créez votre première action de grâce pour exprimer votre gratitude envers Dieu.",
"days_old_single": "({days} jour)",
"days_old_plural": "({days} jours)"
+ },
+ "churn_notification": {
+ "high_title": "Vous nous manquez! 🙏",
+ "high_body": "{days} jours se sont écoulés. Revenez et reconnectez-vous avec votre foi.",
+ "medium_title": "Votre dévotion vous attend 📖",
+ "medium_body": "Ne perdez pas votre série. Lisez la dévotion d'aujourd'hui.",
+ "low_title": "Continuez comme ça! 🔥",
+ "low_body": "Votre dévouement est inspirant!"
}
}
\ No newline at end of file
----------------------------------------
📄 ARCHIVO: i18n/ja.json
Estado: modified (+12/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fja.json
DIFF:
----------------------------------------
@@ -151,7 +151,10 @@
"paypal_error": "PayPalを開く際にエラーが発生しました: {error}",
"paypal_no_app_error": "PayPalを開けませんでした。ウェブブラウザまたはPayPalアプリがインストールされていることを確認してください。",
"backup_option": "バックアップ",
- "backup_subtitle": "データを安全にバックアップして復元"
+ "backup_subtitle": "データを安全にバックアップして復元",
+ "churn_notifications_enabled": "再エンゲージメントリマインダー",
+ "churn_notifications_subtitle": "しばらく不在の時に通知を受け取る",
+ "churn_notifications_disabled": "非アクティブリマインダーは受け取りません"
},
"notifications_config_page": {
"notifications_config": "通知設定",
@@ -658,5 +661,13 @@
"no_thanksgivings_description": "神への感謝を表現するために最初の感謝を作成してください。",
"days_old_single": "({days}日)",
"days_old_plural": "({days}日)"
+ },
+ "churn_notification": {
+ "high_title": "お待ちしています!🙏",
+ "high_body": "{days}日が経過しました。信仰とつながりを取り戻しましょう。",
+ "medium_title": "あなたのデボーションが待っています📖",
+ "medium_body": "連続記録を失わないでください。今日のデボーションを読みましょう。",
+ "low_title": "その調子です!🔥",
+ "low_body": "あなたの献身は励みになります!"
}
}
\ No newline at end of file
----------------------------------------
📄 ARCHIVO: i18n/pt.json
Estado: modified (+12/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/i18n%2Fpt.json
DIFF:
----------------------------------------
@@ -151,7 +151,10 @@
"paypal_no_app_error": "Não foi possível abrir o PayPal. Certifique-se de ter um navegador web ou aplicativo PayPal instalado.",
"backup_option": "Cópia de segurança",
"backup_subtitle": "Faça backup e restaure seus dados com segurança",
- "voice_sample_text": "Toque para ouvir uma amostra de áudio. Depois salve sua preferência."
+ "voice_sample_text": "Toque para ouvir uma amostra de áudio. Depois salve sua preferência.",
+ "churn_notifications_enabled": "Lembretes de Re-engajamento",
+ "churn_notifications_subtitle": "Receba notificações quando estiver ausente",
+ "churn_notifications_disabled": "Você não receberá lembretes de inatividade"
},
"notifications_config_page": {
"notifications_config": "Configuração de notificações",
@@ -658,5 +661,13 @@
"no_thanksgivings_description": "Crie sua primeira ação de graças para expressar sua gratidão a Deus.",
"days_old_single": "({days} dia)",
"days_old_plural": "({days} dias)"
+ },
+ "churn_notification": {
+ "high_title": "Sentimos sua falta! 🙏",
+ "high_body": "Já se passaram {days} dias. Volte e conecte-se com sua fé.",
+ "medium_title": "Seu devocional está esperando 📖",
+ "medium_body": "Não perca sua sequência. Leia o devocional de hoje.",
+ "low_title": "Continue assim! 🔥",
+ "low_body": "Sua dedicação é inspiradora!"
}
}
\ No newline at end of file
----------------------------------------
📄 ARCHIVO: lib/main.dart
Estado: modified (+93/-1)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fmain.dart
DIFF:
----------------------------------------
@@ -18,11 +18,13 @@ import 'package:devocional_nuevo/providers/localization_provider.dart';
import 'package:devocional_nuevo/services/connectivity_service.dart';
import 'package:devocional_nuevo/services/google_drive_auth_service.dart';
import 'package:devocional_nuevo/services/google_drive_backup_service.dart';
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
import 'package:devocional_nuevo/services/notification_service.dart';
import 'package:devocional_nuevo/services/onboarding_service.dart';
import 'package:devocional_nuevo/services/service_locator.dart';
import 'package:devocional_nuevo/services/spiritual_stats_service.dart';
import 'package:devocional_nuevo/services/tts/i_tts_service.dart';
+import 'package:devocional_nuevo/utils/churn_monitoring_helper.dart';
import 'package:devocional_nuevo/splash_screen.dart';
import 'package:devocional_nuevo/utils/constants.dart';
import 'package:devocional_nuevo/utils/theme_constants.dart';
@@ -38,6 +40,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
import 'package:timezone/data/latest_all.dart' as tzdata;
import 'package:timezone/timezone.dart' as tz;
@@ -178,15 +181,80 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState();
}
-class _MyAppState extends State<MyApp> {
+class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
late Future<bool> _initializationFuture;
+ // Remove in-memory timestamp - will use SharedPreferences instead
@override
void initState() {
super.initState();
+ WidgetsBinding.instance.addObserver(this);
_initializationFuture = _initializeApp();
}
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ super.dispose();
+ }
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) {
+ if (state == AppLifecycleState.resumed) {
+ _performDailyChurnCheckIfNeeded();
+ }
+ }
+
+ Future<void> _performDailyChurnCheckIfNeeded() async {
+ // GAP-6: Check if feature is enabled
+ if (!serviceLocator.isRegistered<ChurnPredictionService>()) {
+ return;
+ }
+
+ try {
+ // Issue #2: Persist last check timestamp in SharedPreferences
+ final prefs = await SharedPreferences.getInstance();
+ final lastCheckString = prefs.getString('churn_last_check_timestamp');
+ final now = DateTime.now().toUtc(); // Issue #4: Use UTC
+
+ DateTime? lastCheck;
+ if (lastCheckString != null) {
+ try {
+ lastCheck = DateTime.parse(lastCheckString);
+ } catch (e) {
+ developer.log('Error parsing last check timestamp: $e',
+ name: 'MainApp');
+ }
+ }
+
+ // Check if 24 hours have passed since last check
+ if (lastCheck == null || now.difference(lastCheck).inHours >= 24) {
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Save timestamp after successful check
+ await prefs.setString(
+ 'churn_last_check_timestamp', now.toIso8601String());
+
+ developer.log(
+ 'Daily churn check completed',
+ name: 'MainApp',
+ );
+ } else {
+ final hoursSinceLastCheck = now.difference(lastCheck).inHours;
+ developer.log(
+ 'Skipping churn check - only $hoursSinceLastCheck hours since last check',
+ name: 'MainApp',
+ );
+ }
+ } catch (e) {
+ developer.log(
+ 'Daily churn check failed: $e',
+ name: 'MainApp',
+ error: e,
+ );
+ }
+ }
+
/// Metodo unificado que inicializa servicios y verifica onboarding
Future<bool> _initializeApp() async {
try {
@@ -507,6 +575,30 @@ class _AppInitializerState extends State<AppInitializer> {
name: 'MainApp',
);
}
+
+ // Churn prediction initial check
+ // GAP-6: Check if feature is enabled before calling
+ try {
+ if (serviceLocator.isRegistered<ChurnPredictionService>()) {
+ await ChurnMonitoringHelper.performDailyCheck();
+ developer.log(
+ 'AppInitializer: Initial churn check completed',
+ name: 'MainApp',
+ );
+ } else {
+ developer.log(
+ 'AppInitializer: Churn prediction feature disabled',
+ name: 'MainApp',
+ );
+ }
+ } catch (e) {
+ developer.log(
+ 'ERROR: Failed to initialize churn service: $e',
+ name: 'MainApp',
+ error: e,
+ );
+ // No es crítico, la app puede continuar funcionando
+ }
}
// Inicializar datos de la aplicación
----------------------------------------
📄 ARCHIVO: lib/pages/settings_page.dart
Estado: modified (+94/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fpages%2Fsettings_page.dart
DIFF:
----------------------------------------
@@ -30,6 +30,7 @@ class SettingsPage extends StatefulWidget {
class _SettingsPageState extends State<SettingsPage> {
double _ttsSpeed = 0.4;
+ bool _churnNotificationsEnabled = true; // Default: ON
// Get VoiceSettingsService instance from the Service Locator
late final VoiceSettingsService _voiceSettingsService =
@@ -46,6 +47,7 @@ class _SettingsPageState extends State<SettingsPage> {
_loadTtsSettings();
_loadFeatureFlags();
_loadSavedVoices();
+ _loadChurnNotificationPreference();
}
Future<void> _loadTtsSettings() async {
@@ -99,6 +101,61 @@ class _SettingsPageState extends State<SettingsPage> {
prefs.getString('tts_voice_name_$language');
}
+ Future<void> _loadChurnNotificationPreference() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final enabled =
+ prefs.getBool('churn_notifications_enabled') ?? true; // Default: ON
+ if (mounted) {
+ setState(() {
+ _churnNotificationsEnabled = enabled;
+ });
+ }
+ } catch (e) {
+ developer.log('Error loading churn notification preference: $e');
+ }
+ }
+
+ Future<void> _updateChurnNotifications(bool value) async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool('churn_notifications_enabled', value);
+
+ if (mounted) {
+ setState(() {
+ _churnNotificationsEnabled = value;
+ });
+
+ // Show feedback to user
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ value
+ ? 'settings.churn_notifications_enabled'.tr()
+ : 'settings.churn_notifications_disabled'.tr(),
+ ),
+ duration: const Duration(seconds: 2),
+ ),
+ );
+ }
+
+ developer.log(
+ 'Churn notifications ${value ? "enabled" : "disabled"}',
+ name: 'SettingsPage',
+ );
+ } catch (e) {
+ developer.log('Error updating churn notification preference: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Error updating notification preference: $e'),
+ backgroundColor: Colors.red,
+ ),
+ );
+ }
+ }
+ }
+
Future<void> _onSpeedChanged(double value) async {
try {
final devocionalProvider = Provider.of<DevocionalProvider>(
@@ -400,6 +457,43 @@ class _SettingsPageState extends State<SettingsPage> {
const SizedBox(height: 20),
+ // Notifications Settings
+ Text(
+ 'Notifications',
+ style: textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ color: colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 15),
+
+ // Churn Notification Toggle
+ SwitchListTile(
+ value: _churnNotificationsEnabled,
+ onChanged: _updateChurnNotifications,
+ title: Text(
+ 'settings.churn_notifications_enabled'.tr(),
+ style: textTheme.bodyMedium?.copyWith(
+ fontSize: 16,
+ color: colorScheme.onSurface,
+ ),
+ ),
+ subtitle: Text(
+ 'settings.churn_notifications_subtitle'.tr(),
+ style: textTheme.bodySmall?.copyWith(
+ color: colorScheme.onSurfaceVariant,
+ fontSize: 12,
+ ),
+ ),
+ secondary: Icon(
+ Icons.notifications_active,
+ color: colorScheme.primary,
+ ),
+ activeColor: colorScheme.primary,
+ ),
+
+ const SizedBox(height: 20),
+
// Backup Settings - conditional display (ahora habilitado)
// if (_showBackupSection) ...[
// InkWell(
----------------------------------------
📄 ARCHIVO: lib/services/churn_prediction_service.dart
Estado: added (+570/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fservices%2Fchurn_prediction_service.dart
DIFF:
----------------------------------------
@@ -0,0 +1,570 @@
+// lib/services/churn_prediction_service.dart
+
+import 'dart:developer' as developer;
+import 'package:shared_preferences/shared_preferences.dart';
+import '../models/spiritual_stats_model.dart';
+import '../services/spiritual_stats_service.dart';
+import '../services/notification_service.dart';
+import '../services/localization_service.dart';
+import '../services/service_locator.dart';
+
+/// Risk levels for user churn prediction
+enum ChurnRiskLevel {
+ unknown, // Insufficient data for prediction
+ low, // User is actively engaged
+ medium, // User shows signs of decreased engagement
+ high, // User is at high risk of churning
+}
+
+/// Model for churn prediction result
+class ChurnPrediction {
+ final ChurnRiskLevel riskLevel;
+ final double riskScore; // 0.0 to 1.0
+ final int daysSinceLastActivity;
+ final bool shouldSendNotification;
+ final String reason;
+ final DateTime calculatedAt;
+
+ ChurnPrediction({
+ required this.riskLevel,
+ required this.riskScore,
+ required this.daysSinceLastActivity,
+ required this.shouldSendNotification,
+ required this.reason,
+ required this.calculatedAt,
+ });
+
+ Map<String, dynamic> toJson() {
+ return {
+ 'riskLevel': riskLevel.toString(),
+ 'riskScore': riskScore,
+ 'daysSinceLastActivity': daysSinceLastActivity,
+ 'shouldSendNotification': shouldSendNotification,
+ 'reason': reason,
+ 'calculatedAt': calculatedAt.toIso8601String(),
+ };
+ }
+}
+
+/// Validation for minimum data requirements
+class _ChurnValidation {
+ static const int minReadings = 3;
+ static const int minAccountAgeDays = 7;
+
+ static bool hasMinimumData(SpiritualStats stats) {
+ // Check if user has enough data for meaningful prediction
+ if (stats.totalDevocionalesRead < minReadings) {
+ return false;
+ }
+
+ // Check if account is old enough
+ if (stats.lastActivityDate != null) {
+ final accountAge = DateTime.now()
+ .toUtc()
+ .difference(stats.lastActivityDate!.toUtc())
+ .inDays
+ .abs();
+ if (accountAge < minAccountAgeDays &&
+ stats.totalDevocionalesRead < minReadings) {
+ return false;
+ }
+ }
+
+ // Must have last activity date
+ if (stats.lastActivityDate == null && stats.totalDevocionalesRead == 0) {
+ return false;
+ }
+
+ return true;
+ }
+}
+
+/// Internal cache for engagement metrics
+class _MetricsCache {
+ Map<String, dynamic>? _cachedSummary;
+ DateTime? _cacheTimestamp;
+ static const _cacheDuration = Duration(minutes: 5);
+
+ bool get isValid =>
+ _cacheTimestamp != null &&
+ DateTime.now().toUtc().difference(_cacheTimestamp!) < _cacheDuration;
+
+ void set(Map<String, dynamic> summary) {
+ _cachedSummary = summary;
+ _cacheTimestamp = DateTime.now().toUtc();
+ developer.log(
+ 'Metrics cache SET - valid for ${_cacheDuration.inMinutes} minutes',
+ name: 'ChurnPredictionService',
+ );
+ }
+
+ Map<String, dynamic>? get() {
+ if (isValid && _cachedSummary != null) {
+ developer.log(
+ 'Metrics cache HIT',
+ name: 'ChurnPredictionService',
+ );
+ return Map<String, dynamic>.from(_cachedSummary!);
+ }
+ developer.log(
+ 'Metrics cache MISS',
+ name: 'ChurnPredictionService',
+ );
+ return null;
+ }
+
+ void invalidate() {
+ _cachedSummary = null;
+ _cacheTimestamp = null;
+ developer.log(
+ 'Metrics cache INVALIDATED',
+ name: 'ChurnPredictionService',
+ );
+ }
+}
+
+/// Service for predicting user churn and triggering engagement notifications
+/// This service is NOT a singleton - use dependency injection via ServiceLocator
+class ChurnPredictionService {
+ final SpiritualStatsService _statsService;
+ final NotificationService _notificationService;
+ final _MetricsCache _cache = _MetricsCache();
+
+ // Shared Preferences key for churn notification setting
+ static const String _prefKeyChurnNotifications =
+ 'churn_notifications_enabled';
+
+ // Configuration constants
+ static const int _inactiveDaysThresholdHigh = 7; // 1 week
+ static const int _inactiveDaysThresholdMedium = 3; // 3 days
+ static const int _minReadingsForPrediction =
+ 3; // Minimum readings to analyze patterns
+
+ // Reading frequency risk constants
+ static const int _inactivityDaysForPenalty =
+ 1; // Days threshold for inactivity penalty
+ static const double _inactivityPenaltyScore =
+ 0.2; // Penalty score for inactive users
+
+ ChurnPredictionService({
+ required SpiritualStatsService statsService,
+ required NotificationService notificationService,
+ }) : _statsService = statsService,
+ _notificationService = notificationService;
+
+ /// Analyze user behavior and predict churn risk
+ Future<ChurnPrediction> predictChurnRisk() async {
+ try {
+ final stats = await _statsService.getStats();
+
+ // GAP-2: Validate minimum data requirements
+ if (!_ChurnValidation.hasMinimumData(stats)) {
+ developer.log(
+ 'Insufficient data for churn prediction - returning unknown risk',
+ name: 'ChurnPredictionService',
+ );
+
+ return ChurnPrediction(
+ riskLevel: ChurnRiskLevel.unknown,
+ riskScore: 0.0,
+ daysSinceLastActivity: 0,
+ shouldSendNotification: false,
+ reason: 'Insufficient data for prediction',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ }
+
+ // GAP-5: UTC normalization for timezone consistency
+ final nowUtc = DateTime.now().toUtc();
+
+ // Calculate days since last activity with UTC normalization
+ int daysSinceLastActivity = 999; // Default for null
+ if (stats.lastActivityDate != null) {
+ final lastActivityUtc = stats.lastActivityDate!.toUtc();
+ daysSinceLastActivity = nowUtc.difference(lastActivityUtc).inDays;
+
+ // Safety check: prevent negative values from date arithmetic
+ if (daysSinceLastActivity < 0) {
+ developer.log(
+ 'WARNING: Negative days since last activity detected, using 0',
+ name: 'ChurnPredictionService',
+ );
+ daysSinceLastActivity = 0;
+ }
+ }
+
+ // Calculate churn risk score (0.0 to 1.0)
+ final riskScore = _calculateRiskScore(stats, daysSinceLastActivity);
+
+ // Determine risk level
+ final riskLevel = _determineRiskLevel(riskScore, daysSinceLastActivity);
+
+ // Decide if notification should be sent
+ final shouldSendNotification = _shouldSendNotification(
+ riskLevel,
+ daysSinceLastActivity,
+ stats,
+ );
+
+ // Generate reason for the prediction
+ final reason = _generateReason(
+ stats,
+ daysSinceLastActivity,
+ riskScore,
+ riskLevel,
+ );
+
+ final prediction = ChurnPrediction(
+ riskLevel: riskLevel,
+ riskScore: riskScore,
+ daysSinceLastActivity: daysSinceLastActivity,
+ shouldSendNotification: shouldSendNotification,
+ reason: reason,
+ calculatedAt: nowUtc,
+ );
+
+ // GAP-4: Basic analytics logging
+ developer.log(
+ 'ChurnAnalytics: prediction_made '
+ 'risk=${riskLevel.name} '
+ 'score=${riskScore.toStringAsFixed(2)} '
+ 'inactive_days=$daysSinceLastActivity',
+ name: 'ChurnPredictionService',
+ );
+
+ return prediction;
+ } catch (e, stackTrace) {
+ developer.log(
+ 'Error predicting churn risk: $e',
+ name: 'ChurnPredictionService',
+ error: e,
+ stackTrace: stackTrace,
+ );
+
+ // GAP-4: Log analytics for failures
+ developer.log(
+ 'ChurnAnalytics: prediction_failed error=$e',
+ name: 'ChurnPredictionService',
+ );
+
+ // Return safe default - unknown risk, no notification
+ return ChurnPrediction(
+ riskLevel: ChurnRiskLevel.unknown,
+ riskScore: 0.0,
+ daysSinceLastActivity: 0,
+ shouldSendNotification: false,
+ reason: 'Error calculating churn risk',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ }
+ }
+
+ /// Calculate churn risk score based on multiple factors
+ double _calculateRiskScore(SpiritualStats stats, int daysSinceLastActivity) {
+ double score = 0.0;
+
+ // GAP-2: Safety check for negative or invalid values
+ if (daysSinceLastActivity < 0) {
+ developer.log(
+ 'WARNING: Invalid daysSinceLastActivity in score calculation',
+ name: 'ChurnPredictionService',
+ );
+ daysSinceLastActivity = 0;
+ }
+
+ // Factor 1: Days since last activity (weight: 40%)
+ if (daysSinceLastActivity >= _inactiveDaysThresholdHigh) {
+ score += 0.4;
+ } else if (daysSinceLastActivity >= _inactiveDaysThresholdMedium) {
+ score += 0.2;
+ }
+
+ // Factor 2: Streak decline (weight: 30%)
+ // GAP-2: Prevent division by zero
+ if (stats.longestStreak > 0) {
+ final streakDeclineRatio =
+ 1.0 - (stats.currentStreak / stats.longestStreak);
+ score += streakDeclineRatio * 0.3;
+ }
+
+ // Factor 3: Reading frequency (weight: 20%)
+ if (stats.totalDevocionalesRead < _minReadingsForPrediction) {
+ score += _inactivityPenaltyScore; // New users are at risk
+ } else if (daysSinceLastActivity > _inactivityDaysForPenalty) {
+ // Check recent reading decline based on days since last activity
+ // A user who was active but stopped is at higher risk
+ score += _inactivityPenaltyScore;
+ }
+
+ // Factor 4: Zero current streak (weight: 10%)
+ if (stats.currentStreak == 0 && stats.totalDevocionalesRead > 0) {
+ score += 0.1;
+ }
+
+ // GAP-2: Clamp to valid range
+ return score.clamp(0.0, 1.0);
+ }
+
+ /// Determine risk level from risk score
+ ChurnRiskLevel _determineRiskLevel(
+ double riskScore, int daysSinceLastActivity) {
+ if (riskScore >= 0.6 ||
+ daysSinceLastActivity >= _inactiveDaysThresholdHigh) {
+ return ChurnRiskLevel.high;
+ } else if (riskScore >= 0.3 ||
+ daysSinceLastActivity >= _inactiveDaysThresholdMedium) {
+ return ChurnRiskLevel.medium;
+ }
+ return ChurnRiskLevel.low;
+ }
+
+ /// Decide if notification should be sent based on risk level and current state
+ bool _shouldSendNotification(
+ ChurnRiskLevel riskLevel,
+ int daysSinceLastActivity,
+ SpiritualStats stats,
+ ) {
+ // Don't send notifications if user has never read anything
+ if (stats.totalDevocionalesRead == 0) {
+ return false;
+ }
+
+ // Send notification for high risk users
+ if (riskLevel == ChurnRiskLevel.high) {
+ return true;
+ }
+
+ // Send notification for medium risk if enough time has passed
+ if (riskLevel == ChurnRiskLevel.medium && daysSinceLastActivity >= 3) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Generate human-readable reason for the prediction
+ String _generateReason(
+ SpiritualStats stats,
+ int daysSinceLastActivity,
+ double riskScore,
+ ChurnRiskLevel riskLevel,
+ ) {
+ if (stats.totalDevocionalesRead == 0) {
+ return 'New user with no activity';
+ }
+
+ if (daysSinceLastActivity >= _inactiveDaysThresholdHigh) {
+ return 'User inactive for $daysSinceLastActivity days';
+ }
+
+ if (daysSinceLastActivity >= _inactiveDaysThresholdMedium) {
+ return 'User activity declining ($daysSinceLastActivity days since last read)';
+ }
+
+ if (stats.currentStreak == 0 && stats.longestStreak > 0) {
+ return 'User lost their streak (was ${stats.longestStreak} days)';
+ }
+
+ if (riskLevel == ChurnRiskLevel.low) {
+ return 'User actively engaged (${stats.currentStreak} day streak)';
+ }
+
+ return 'User engagement at medium risk (score: ${riskScore.toStringAsFixed(2)})';
+ }
+
+ /// Send engagement notification based on churn risk
+ /// @deprecated Use ChurnMonitoringHelper.performDailyCheck() instead
+ /// This method is kept for backward compatibility with existing tests
+ /// GAP-3: Notification logic should be handled by ChurnMonitoringHelper
+ @Deprecated('Use ChurnMonitoringHelper for notification handling')
+ Future<void> sendChurnPreventionNotification(
+ ChurnPrediction prediction) async {
+ if (!prediction.shouldSendNotification) {
+ developer.log(
+ 'Skipping notification - not needed for current risk level',
+ name: 'ChurnPredictionService',
+ );
+ return;
+ }
+
+ try {
+ // Check if churn notifications are enabled by user
+ final prefs = await SharedPreferences.getInstance();
+ final churnNotificationsEnabled =
+ prefs.getBool(_prefKeyChurnNotifications) ?? true; // Default: ON
+
+ if (!churnNotificationsEnabled) {
+ developer.log(
+ 'Churn notifications disabled by user preference - skipping',
+ name: 'ChurnPredictionService',
+ );
+ return;
+ }
+
+ final notificationsEnabled =
+ await _notificationService.areNotificationsEnabled();
+ if (!notificationsEnabled) {
+ developer.log(
+ 'Notifications disabled by user - skipping churn prevention notification',
+ name: 'ChurnPredictionService',
+ );
+ return;
+ }
+
+ final title = _getNotificationTitle(prediction.riskLevel);
+ final body = _getNotificationBody(prediction);
+
+ await _notificationService.showImmediateNotification(
+ title,
+ body,
+ payload: 'churn_prevention',
+ id: DateTime.now().toUtc().millisecondsSinceEpoch,
+ );
+
+ developer.log(
+ 'Churn prevention notification sent: $title',
+ name: 'ChurnPredictionService',
+ );
+ } catch (e) {
+ developer.log(
+ 'Error sending churn prevention notification: $e',
+ name: 'ChurnPredictionService',
+ error: e,
+ );
+ }
+ }
+
+ /// Get notification title based on risk level
+ String _getNotificationTitle(ChurnRiskLevel riskLevel) {
+ final localization = getService<LocalizationService>();
+
+ switch (riskLevel) {
+ case ChurnRiskLevel.high:
+ return localization.translate('churn_notification.high_title');
+ case ChurnRiskLevel.medium:
+ return localization.translate('churn_notification.medium_title');
+ case ChurnRiskLevel.low:
+ return localization.translate('churn_notification.low_title');
+ case ChurnRiskLevel.unknown:
+ return 'Notification'; // Fallback, should not send notification for unknown
+ }
+ }
+
+ /// Get notification body based on prediction
+ String _getNotificationBody(ChurnPrediction prediction) {
+ final localization = getService<LocalizationService>();
+
+ switch (prediction.riskLevel) {
+ case ChurnRiskLevel.high:
+ return localization.translate(
+ 'churn_notification.high_body',
+ {'days': prediction.daysSinceLastActivity.toString()},
+ );
+ case ChurnRiskLevel.medium:
+ return localization.translate('churn_notification.medium_body');
+ case ChurnRiskLevel.low:
+ return localization.translate('churn_notification.low_body');
+ case ChurnRiskLevel.unknown:
+ return 'Insufficient data'; // Fallback, should not send notification for unknown
+ }
+ }
+
+ /// Schedule daily churn check (to be called periodically)
+ Future<void> performDailyChurnCheck() async {
+ try {
+ developer.log(
+ 'Performing daily churn check',
+ name: 'ChurnPredictionService',
+ );
+
+ final prediction = await predictChurnRisk();
+
+ if (prediction.shouldSendNotification) {
+ await sendChurnPreventionNotification(prediction);
+ }
+
+ developer.log(
+ 'Daily churn check completed: ${prediction.reason}',
+ name: 'ChurnPredictionService',
+ );
+ } catch (e) {
+ developer.log(
+ 'Error in daily churn check: $e',
+ name: 'ChurnPredictionService',
+ error: e,
+ );
+ }
+ }
+
+ /// Get user engagement summary
+ Future<Map<String, dynamic>> getEngagementSummary() async {
+ try {
+ // Check cache first
+ final cached = _cache.get();
+ if (cached != null) {
+ return cached;
+ }
+
+ final stats = await _statsService.getStats();
+ final prediction = await predictChurnRisk();
+
+ final summary = {
+ 'total_readings': stats.totalDevocionalesRead,
+ 'current_streak': stats.currentStreak,
+ 'longest_streak': stats.longestStreak,
+ 'days_since_last_activity': prediction.daysSinceLastActivity,
+ 'churn_risk_level': prediction.riskLevel.toString(),
+ 'churn_risk_score': prediction.riskScore,
+ 'engagement_status': _getEngagementStatus(prediction.riskLevel),
+ 'last_activity_date': stats.lastActivityDate?.toIso8601String(),
+ };
+
+ // Cache the result
+ _cache.set(summary);
+
+ return summary;
+ } catch (e) {
+ developer.log(
+ 'Error getting engagement summary: $e',
+ name: 'ChurnPredictionService',
+ error: e,
+ );
+ return {};
+ }
+ }
+
+ /// Invalidate the metrics cache (call when user stats change)
+ void invalidateCache() {
+ _cache.invalidate();
+ }
+
+ /// Check if churn notifications are enabled
+ static Future<bool> areChurnNotificationsEnabled() async {
+ final prefs = await SharedPreferences.getInstance();
+ return prefs.getBool(_prefKeyChurnNotifications) ?? true; // Default: ON
+ }
+
+ /// Set churn notification preference
+ static Future<void> setChurnNotificationsEnabled(bool enabled) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_prefKeyChurnNotifications, enabled);
+ developer.log(
+ 'Churn notifications ${enabled ? "enabled" : "disabled"}',
+ name: 'ChurnPredictionService',
+ );
+ }
+
+ /// Get engagement status text
+ String _getEngagementStatus(ChurnRiskLevel riskLevel) {
+ switch (riskLevel) {
+ case ChurnRiskLevel.unknown:
+ return 'Insufficient Data';
+ case ChurnRiskLevel.low:
+ return 'Highly Engaged';
+ case ChurnRiskLevel.medium:
+ return 'Moderately Engaged';
+ case ChurnRiskLevel.high:
+ return 'At Risk';
+ }
+ }
+}
----------------------------------------
📄 ARCHIVO: lib/services/service_locator.dart
Estado: modified (+35/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Fservices%2Fservice_locator.dart
DIFF:
----------------------------------------
@@ -14,10 +14,14 @@
/// and testing.
library;
+import 'dart:developer' as developer;
import 'package:bible_reader_core/bible_reader_core.dart';
import 'package:devocional_nuevo/adapters/http_client_adapter.dart';
import 'package:devocional_nuevo/adapters/storage_adapter.dart';
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
import 'package:devocional_nuevo/services/localization_service.dart';
+import 'package:devocional_nuevo/services/notification_service.dart';
+import 'package:devocional_nuevo/services/spiritual_stats_service.dart';
import 'package:devocional_nuevo/services/tts/i_tts_service.dart';
import 'package:devocional_nuevo/services/tts/voice_settings_service.dart';
import 'package:devocional_nuevo/services/tts_service.dart';
@@ -125,6 +129,37 @@ void setupServiceLocator() {
),
);
+ // GAP-6: Feature flag for churn prediction (compile-time)
+ // Can be controlled via --dart-define=CHURN_ENABLED=true
+ const bool isChurnFeatureEnabled =
+ bool.fromEnvironment('CHURN_ENABLED', defaultValue: true);
+
+ if (isChurnFeatureEnabled) {
+ // Register ChurnPredictionService as a factory (NOT singleton)
+ // Each call creates a new instance for better testability and to avoid state issues
+ // Note: NotificationService is a singleton, so we get the same instance
+ locator.registerFactory<ChurnPredictionService>(
+ () => ChurnPredictionService(
+ statsService: locator.get<SpiritualStatsService>(),
+ notificationService: NotificationService(),
+ ),
+ );
+ developer.log(
+ 'ChurnPredictionService registered (feature enabled)',
+ name: 'ServiceLocator',
+ );
+ } else {
+ developer.log(
+ 'ChurnPredictionService NOT registered (feature disabled)',
+ name: 'ServiceLocator',
+ );
+ }
+
+ // Register SpiritualStatsService as a lazy singleton
+ locator.registerLazySingleton<SpiritualStatsService>(
+ () => SpiritualStatsService(),
+ );
+
// Add more service registrations here as needed
// Example:
// locator.registerLazySingleton<IAnalyticsService>(() => AnalyticsService());
----------------------------------------
📄 ARCHIVO: lib/utils/churn_monitoring_helper.dart
Estado: added (+281/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/lib%2Futils%2Fchurn_monitoring_helper.dart
DIFF:
----------------------------------------
@@ -0,0 +1,281 @@
+// lib/utils/churn_monitoring_helper.dart
+
+import 'dart:developer' as developer;
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
+import 'package:devocional_nuevo/services/notification_service.dart';
+import 'package:devocional_nuevo/services/localization_service.dart';
+import 'package:devocional_nuevo/services/service_locator.dart';
+
+/// Helper class for churn monitoring with rate limiting and analytics
+///
+/// GAP-3: Separates notification logic from prediction service
+/// GAP-4: Adds basic analytics logging
+/// GAP-7: Implements rate limiting (max 2 notifications per week)
+class ChurnMonitoringHelper {
+ // GAP-7: Rate limiting constants
+ static const int _maxNotificationsPerWeek = 2;
+ static const String _prefKeyNotificationHistory = 'churn_notifications_sent';
+ static const Duration _rateLimitWindow = Duration(days: 7);
+
+ /// Perform daily churn check with rate limiting and analytics
+ /// This should be called once per day via background task or app start
+ static Future<void> performDailyCheck() async {
+ try {
+ // Check if churn notifications are enabled by user
+ final prefs = await SharedPreferences.getInstance();
+ final churnNotificationsEnabled =
+ prefs.getBool('churn_notifications_enabled') ?? true;
+
+ if (!churnNotificationsEnabled) {
+ developer.log(
+ 'ChurnAnalytics: check_skipped reason=user_disabled',
+ name: 'ChurnMonitoringHelper',
+ );
+ return;
+ }
+
+ // Get prediction from service
+ final churnService = getService<ChurnPredictionService>();
+ final prediction = await churnService.predictChurnRisk();
+
+ // GAP-4: Log prediction event
+ developer.log(
+ 'ChurnAnalytics: prediction_made '
+ 'risk=${prediction.riskLevel.name} '
+ 'score=${prediction.riskScore.toStringAsFixed(2)} '
+ 'inactive_days=${prediction.daysSinceLastActivity}',
+ name: 'ChurnMonitoringHelper',
+ );
+
+ // GAP-3: Handle notification logic here (separated from service)
+ if (prediction.shouldSendNotification) {
+ // GAP-7: Check rate limiting before sending
+ final canSend = await _canSendNotification();
+ if (canSend) {
+ await _sendChurnNotification(prediction);
+
+ // GAP-4: Log notification sent
+ developer.log(
+ 'ChurnAnalytics: notification_sent level=${prediction.riskLevel.name}',
+ name: 'ChurnMonitoringHelper',
+ );
+
+ // Record notification in history
+ await _recordNotificationSent();
+ } else {
+ developer.log(
+ 'ChurnAnalytics: notification_rate_limited',
+ name: 'ChurnMonitoringHelper',
+ );
+ }
+ }
+
+ developer.log(
+ 'Daily churn monitoring check completed',
+ name: 'ChurnMonitoringHelper',
+ );
+ } catch (e, stackTrace) {
+ // GAP-4: Log errors
+ developer.log(
+ 'ChurnAnalytics: check_failed error=$e',
+ name: 'ChurnMonitoringHelper',
+ error: e,
+ stackTrace: stackTrace,
+ );
+ }
+ }
+
+ /// GAP-7: Check if notification can be sent (rate limiting)
+ static Future<bool> _canSendNotification() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final history = prefs.getStringList(_prefKeyNotificationHistory) ?? [];
+
+ // Parse and filter notifications within the rate limit window
+ final cutoffTime = DateTime.now().toUtc().subtract(_rateLimitWindow);
+
+ final recentNotifications = history
+ .map((timestamp) {
+ try {
+ return DateTime.parse(timestamp);
+ } catch (e) {
+ return null;
+ }
+ })
+ .where((date) => date != null && date.isAfter(cutoffTime))
+ .length;
+
+ return recentNotifications < _maxNotificationsPerWeek;
+ } catch (e) {
+ developer.log(
+ 'Error checking notification rate limit: $e',
+ name: 'ChurnMonitoringHelper',
+ );
+ // On error, allow sending (fail open)
+ return true;
+ }
+ }
+
+ /// GAP-7: Record that a notification was sent
+ static Future<void> _recordNotificationSent() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final history = prefs.getStringList(_prefKeyNotificationHistory) ?? [];
+
+ // Add current timestamp
+ history.add(DateTime.now().toUtc().toIso8601String());
+
+ // Clean up old entries (older than rate limit window)
+ final cutoffTime = DateTime.now().toUtc().subtract(_rateLimitWindow);
+
+ final cleanedHistory = history.where((timestamp) {
+ try {
+ final date = DateTime.parse(timestamp);
+ return date.isAfter(cutoffTime);
+ } catch (e) {
+ return false;
+ }
+ }).toList();
+
+ await prefs.setStringList(_prefKeyNotificationHistory, cleanedHistory);
+ } catch (e) {
+ developer.log(
+ 'Error recording notification history: $e',
+ name: 'ChurnMonitoringHelper',
+ );
+ }
+ }
+
+ /// GAP-3: Send churn notification (notification logic separated from service)
+ static Future<void> _sendChurnNotification(ChurnPrediction prediction) async {
+ try {
+ final notificationService = NotificationService();
+ final localizationService = getService<LocalizationService>();
+
+ // Get localized notification content
+ String title;
+ String body;
+
+ switch (prediction.riskLevel) {
+ case ChurnRiskLevel.high:
+ title =
+ localizationService.translate('churn_notification.high_title');
+ body = localizationService.translate(
+ 'churn_notification.high_body',
+ {'days': prediction.daysSinceLastActivity.toString()},
+ );
+ break;
+ case ChurnRiskLevel.medium:
+ title =
+ localizationService.translate('churn_notification.medium_title');
+ body =
+ localizationService.translate('churn_notification.medium_body');
+ break;
+ default:
+ // Don't send notification for low or unknown risk
+ return;
+ }
+
+ await notificationService.showImmediateNotification(
+ title,
+ body,
+ payload: 'churn_prevention',
+ id: DateTime.now().toUtc().millisecondsSinceEpoch,
+ );
+
+ developer.log(
+ 'Churn notification sent: $title',
+ name: 'ChurnMonitoringHelper',
+ );
+ } catch (e) {
+ developer.log(
+ 'Error sending churn notification: $e',
+ name: 'ChurnMonitoringHelper',
+ error: e,
+ );
+ }
+ }
+
+ /// Get current engagement summary for the user
+ static Future<Map<String, dynamic>> getEngagementSummary() async {
+ try {
+ final churnService = getService<ChurnPredictionService>();
+ return await churnService.getEngagementSummary();
+ } catch (e) {
+ developer.log(
+ 'Error getting engagement summary: $e',
+ name: 'ChurnMonitoringHelper',
+ error: e,
+ );
+ return {};
+ }
+ }
+
+ /// Check if user is at risk and return risk level
+ static Future<ChurnRiskLevel> checkUserRisk() async {
+ try {
+ final churnService = getService<ChurnPredictionService>();
+ final prediction = await churnService.predictChurnRisk();
+ return prediction.riskLevel;
+ } catch (e) {
+ developer.log(
+ 'Error checking user risk: $e',
+ name: 'ChurnMonitoringHelper',
+ error: e,
+ );
+ return ChurnRiskLevel.unknown;
+ }
+ }
+
+ /// Manually trigger a churn prevention notification (for testing)
+ static Future<void> sendChurnPreventionNotification() async {
+ try {
+ final churnService = getService<ChurnPredictionService>();
+ final prediction = await churnService.predictChurnRisk();
+
+ if (prediction.shouldSendNotification) {
+ // Bypass rate limiting for manual sends
+ await _sendChurnNotification(prediction);
+ developer.log(
+ 'Churn prevention notification sent manually',
+ name: 'ChurnMonitoringHelper',
+ );
+ } else {
+ developer.log(
+ 'Notification not needed for current risk level',
+ name: 'ChurnMonitoringHelper',
+ );
+ }
+ } catch (e) {
+ developer.log(
+ 'Error sending churn prevention notification: $e',
+ name: 'ChurnMonitoringHelper',
+ error: e,
+ );
+ }
+ }
+
+ /// Get notification history count (for debugging)
+ static Future<int> getNotificationHistoryCount() async {
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final history = prefs.getStringList(_prefKeyNotificationHistory) ?? [];
+
+ final cutoffTime = DateTime.now().toUtc().subtract(_rateLimitWindow);
+
+ return history
+ .map((timestamp) {
+ try {
+ return DateTime.parse(timestamp);
+ } catch (e) {
+ return null;
+ }
+ })
+ .where((date) => date != null && date.isAfter(cutoffTime))
+ .length;
+ } catch (e) {
+ return 0;
+ }
+ }
+}
----------------------------------------
📄 ARCHIVO: pubspec.lock
Estado: modified (+14/-14)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/pubspec.lock
DIFF:
----------------------------------------
@@ -791,26 +791,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
- sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+ sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.dev"
source: hosted
- version: "11.0.2"
+ version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+ sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
- version: "3.0.10"
+ version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
- sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.0.1"
lints:
dependency: transitive
description:
@@ -1388,26 +1388,26 @@ packages:
dependency: "direct dev"
description:
name: test
- sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb"
+ sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.dev"
source: hosted
- version: "1.26.2"
+ version: "1.25.15"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
+ sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
- version: "0.7.6"
+ version: "0.7.4"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a"
+ sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev"
source: hosted
- version: "0.6.11"
+ version: "0.6.8"
timezone:
dependency: "direct main"
description:
@@ -1516,10 +1516,10 @@ packages:
dependency: transitive
description:
name: vector_math
- sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.1.4"
vm_service:
dependency: transitive
description:
----------------------------------------
📄 ARCHIVO: test/integration/churn_monitoring_integration_test.dart
Estado: added (+171/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Fintegration%2Fchurn_monitoring_integration_test.dart
DIFF:
----------------------------------------
@@ -0,0 +1,171 @@
+// test/integration/churn_monitoring_integration_test.dart
+
+import 'package:devocional_nuevo/models/spiritual_stats_model.dart';
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
+import 'package:devocional_nuevo/services/localization_service.dart';
+import 'package:devocional_nuevo/services/notification_service.dart';
+import 'package:devocional_nuevo/services/service_locator.dart';
+import 'package:devocional_nuevo/services/spiritual_stats_service.dart';
+import 'package:devocional_nuevo/utils/churn_monitoring_helper.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+// Mock services
+class MockNotificationService extends Mock implements NotificationService {}
+
+class MockLocalizationService extends Mock implements LocalizationService {}
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('ChurnMonitoring Integration Tests - End-to-End Workflows', () {
+ late SpiritualStatsService statsService;
+ late MockNotificationService mockNotificationService;
+ late MockLocalizationService mockLocalizationService;
+ late ChurnPredictionService churnService;
+ late ServiceLocator serviceLocator;
+
+ setUp(() async {
+ // Reset SharedPreferences for clean state
+ SharedPreferences.setMockInitialValues({});
+
+ // Setup service locator
+ serviceLocator = ServiceLocator();
+ serviceLocator.reset();
+
+ // Create services
+ statsService = SpiritualStatsService();
+ mockNotificationService = MockNotificationService();
+ mockLocalizationService = MockLocalizationService();
+
+ // Register mocks
+ serviceLocator.registerFactory<LocalizationService>(
+ () => mockLocalizationService,
+ );
+
+ // Setup localization mocks
+ when(() => mockLocalizationService.translate(any()))
+ .thenReturn('Translated');
+ when(() => mockLocalizationService.translate(any(), any()))
+ .thenReturn('Translated with params');
+ when(() => mockLocalizationService
+ .translate('achievements.first_read_title')).thenReturn('First Step');
+ when(() => mockLocalizationService
+ .translate('achievements.first_read_description'))
+ .thenReturn('Read your first devotional');
+
+ // Mock notification service methods
+ when(() => mockNotificationService.areNotificationsEnabled())
+ .thenAnswer((_) async => true);
+ when(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: any(named: 'payload'),
+ id: any(named: 'id'),
+ )).thenAnswer((_) async {});
+
+ churnService = ChurnPredictionService(
+ statsService: statsService,
+ notificationService: mockNotificationService,
+ );
+
+ // Reset stats for clean test state
+ await statsService.resetStats();
+ });
+
+ tearDown(() {
+ serviceLocator.reset();
+ });
+
+ test('performs daily check workflow for new user', () async {
+ // Arrange: New user with no activity
+ final initialStats = await statsService.getStats();
+ expect(initialStats.totalDevocionalesRead, equals(0));
+
+ // Act: Perform daily check (uses helper which calls predictChurnRisk)
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Assert: Check completed without errors and risk calculated
+ final prediction = await churnService.predictChurnRisk();
+ expect(
+ prediction.riskLevel,
+ isIn([
+ ChurnRiskLevel.unknown,
+ ChurnRiskLevel.low,
+ ChurnRiskLevel.medium,
+ ChurnRiskLevel.high,
+ ]));
+ // New user with no data should return unknown
+ expect(prediction.riskLevel, equals(ChurnRiskLevel.unknown));
+ });
+
+ test('retrieves engagement summary with correct structure', () async {
+ // Arrange: User with no readings
+ final summary = await churnService.getEngagementSummary();
+
+ // Assert: Verify structure
+ expect(summary, isNotEmpty);
+ expect(summary.containsKey('total_readings'), isTrue);
+ expect(summary.containsKey('current_streak'), isTrue);
+ expect(summary.containsKey('churn_risk_level'), isTrue);
+ expect(summary.containsKey('engagement_status'), isTrue);
+ });
+
+ test('detects high risk for inactive user', () async {
+ // Arrange: Setup user with old activity date
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 5,
+ currentStreak: 0,
+ longestStreak: 5,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 8)),
+ );
+ await statsService.saveStats(stats);
+
+ // Act: Check user risk
+ final prediction = await churnService.predictChurnRisk();
+
+ // Assert: Inactive user should have high risk
+ expect(prediction.riskLevel, equals(ChurnRiskLevel.high));
+ expect(prediction.daysSinceLastActivity, greaterThanOrEqualTo(7));
+ });
+
+ test('churn check performance meets SLA', () async {
+ // Arrange: Setup basic stats
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 1,
+ currentStreak: 1,
+ longestStreak: 1,
+ lastActivityDate: DateTime.now(),
+ );
+ await statsService.saveStats(stats);
+
+ // Act: Measure performance
+ final stopwatch = Stopwatch()..start();
+ await churnService.performDailyChurnCheck();
+ stopwatch.stop();
+
+ // Assert: Should complete within 2 seconds (SLA)
+ expect(stopwatch.elapsedMilliseconds, lessThan(2000),
+ reason: 'Churn check should complete within 2 second SLA');
+ });
+
+ test('handles edge case of user with no last activity date', () async {
+ // Arrange: User stats with null last activity
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 0,
+ currentStreak: 0,
+ longestStreak: 0,
+ lastActivityDate: null,
+ );
+ await statsService.saveStats(stats);
+
+ // Act: Predict churn risk
+ final prediction = await churnService.predictChurnRisk();
+
+ // Assert: Should handle null gracefully
+ expect(prediction.riskLevel, isNotNull);
+ expect(prediction.shouldSendNotification, isFalse);
+ });
+ });
+}
----------------------------------------
📄 ARCHIVO: test/services/churn_prediction_service_test.dart
Estado: added (+404/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Fservices%2Fchurn_prediction_service_test.dart
DIFF:
----------------------------------------
@@ -0,0 +1,404 @@
+// test/services/churn_prediction_service_test.dart
+
+import 'package:devocional_nuevo/models/spiritual_stats_model.dart';
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
+import 'package:devocional_nuevo/services/localization_service.dart';
+import 'package:devocional_nuevo/services/notification_service.dart';
+import 'package:devocional_nuevo/services/service_locator.dart';
+import 'package:devocional_nuevo/services/spiritual_stats_service.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+// Mock classes
+class MockSpiritualStatsService extends Mock implements SpiritualStatsService {}
+
+class MockNotificationService extends Mock implements NotificationService {}
+
+class MockLocalizationService extends Mock implements LocalizationService {}
+
+void main() {
+ late MockSpiritualStatsService mockStatsService;
+ late MockNotificationService mockNotificationService;
+ late MockLocalizationService mockLocalizationService;
+ late ChurnPredictionService churnPredictionService;
+ late ServiceLocator serviceLocator;
+
+ setUp(() async {
+ // Setup SharedPreferences with mock
+ SharedPreferences.setMockInitialValues({});
+
+ // Reset and setup service locator
+ serviceLocator = ServiceLocator();
+ serviceLocator.reset();
+
+ mockStatsService = MockSpiritualStatsService();
+ mockNotificationService = MockNotificationService();
+ mockLocalizationService = MockLocalizationService();
+
+ // Register mocks in service locator
+ serviceLocator.registerFactory<LocalizationService>(
+ () => mockLocalizationService,
+ );
+
+ // Setup default localization responses
+ when(() =>
+ mockLocalizationService.translate('churn_notification.high_title'))
+ .thenReturn('We miss you! 🙏');
+ when(() => mockLocalizationService
+ .translate('churn_notification.medium_title'))
+ .thenReturn('Your devotional is waiting 📖');
+ when(() =>
+ mockLocalizationService.translate('churn_notification.low_title'))
+ .thenReturn('Keep it up! 🔥');
+ when(() => mockLocalizationService.translate(
+ 'churn_notification.high_body', any()))
+ .thenReturn(
+ 'X days have passed. Come back and connect with your faith.');
+ when(() =>
+ mockLocalizationService.translate('churn_notification.medium_body'))
+ .thenReturn('Don\'t lose your streak. Read today\'s devotional.');
+ when(() => mockLocalizationService.translate('churn_notification.low_body'))
+ .thenReturn('Your dedication is inspiring!');
+
+ churnPredictionService = ChurnPredictionService(
+ statsService: mockStatsService,
+ notificationService: mockNotificationService,
+ );
+ });
+
+ tearDown(() {
+ serviceLocator.reset();
+ });
+
+ group('ChurnPredictionService - Risk Calculation', () {
+ test('predicts LOW risk for actively engaged user', () async {
+ // Arrange: User with current streak, recent activity
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 5,
+ longestStreak: 7,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 1)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.low);
+ expect(prediction.riskScore, lessThan(0.3));
+ expect(prediction.shouldSendNotification, false);
+ expect(prediction.daysSinceLastActivity, equals(1));
+ });
+
+ test('predicts MEDIUM risk for user with declining activity', () async {
+ // Arrange: User inactive for 3 days with good history but lost streak
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 2,
+ longestStreak: 5,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 3)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.medium);
+ expect(prediction.riskScore, greaterThanOrEqualTo(0.3));
+ expect(prediction.daysSinceLastActivity, equals(3));
+ expect(prediction.shouldSendNotification, true);
+ });
+
+ test('predicts HIGH risk for user inactive for 7+ days', () async {
+ // Arrange: User inactive for a week
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 8,
+ currentStreak: 0,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 7)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.high);
+ expect(prediction.riskScore, greaterThanOrEqualTo(0.6));
+ expect(prediction.daysSinceLastActivity, equals(7));
+ expect(prediction.shouldSendNotification, true);
+ });
+
+ test('predicts HIGH risk for user who lost long streak', () async {
+ // Arrange: User with lost streak
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 15,
+ currentStreak: 0,
+ longestStreak: 15,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 5)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.high);
+ expect(prediction.daysSinceLastActivity, equals(5));
+ expect(prediction.shouldSendNotification, true);
+ });
+
+ test('handles new user with no activity correctly', () async {
+ // Arrange: Brand new user
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 0,
+ currentStreak: 0,
+ longestStreak: 0,
+ lastActivityDate: null,
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.unknown); // Insufficient data
+ expect(prediction.shouldSendNotification, false); // Don't spam new users
+ expect(prediction.riskScore, equals(0.0)); // No risk score for unknown
+ });
+
+ test('handles user with minimal readings', () async {
+ // Arrange: User with only 1 reading (insufficient for prediction)
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 1,
+ currentStreak: 1,
+ longestStreak: 1,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 2)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel,
+ ChurnRiskLevel.unknown); // Less than minimum 3 readings
+ expect(prediction.riskScore, equals(0.0));
+ expect(prediction.shouldSendNotification, false);
+ });
+ });
+
+ group('ChurnPredictionService - Notification Sending', () {
+ test('sends notification for high risk user when enabled', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 0,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 8)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+ when(() => mockNotificationService.areNotificationsEnabled())
+ .thenAnswer((_) async => true);
+ when(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: any(named: 'payload'),
+ id: any(named: 'id'),
+ )).thenAnswer((_) async {});
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+ await churnPredictionService.sendChurnPreventionNotification(prediction);
+
+ // Assert
+ verify(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: 'churn_prevention',
+ id: any(named: 'id'),
+ )).called(1);
+ });
+
+ test('does not send notification when disabled by user', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 0,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 8)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+ when(() => mockNotificationService.areNotificationsEnabled())
+ .thenAnswer((_) async => false);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+ await churnPredictionService.sendChurnPreventionNotification(prediction);
+
+ // Assert
+ verifyNever(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: any(named: 'payload'),
+ id: any(named: 'id'),
+ ));
+ });
+
+ test('does not send notification for low risk user', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 5,
+ longestStreak: 7,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 1)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+ await churnPredictionService.sendChurnPreventionNotification(prediction);
+
+ // Assert
+ verifyNever(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: any(named: 'payload'),
+ id: any(named: 'id'),
+ ));
+ });
+ });
+
+ group('ChurnPredictionService - Daily Check', () {
+ test('performs daily churn check successfully', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 5,
+ currentStreak: 0,
+ longestStreak: 5,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 5)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+ when(() => mockNotificationService.areNotificationsEnabled())
+ .thenAnswer((_) async => true);
+ when(() => mockNotificationService.showImmediateNotification(
+ any(),
+ any(),
+ payload: any(named: 'payload'),
+ id: any(named: 'id'),
+ )).thenAnswer((_) async {});
+
+ // Act
+ await churnPredictionService.performDailyChurnCheck();
+
+ // Assert
+ verify(() => mockStatsService.getStats()).called(1);
+ verify(() => mockNotificationService.areNotificationsEnabled()).called(1);
+ });
+ });
+
+ group('ChurnPredictionService - Engagement Summary', () {
+ test('generates engagement summary correctly', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 15,
+ currentStreak: 3,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 2)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final summary = await churnPredictionService.getEngagementSummary();
+
+ // Assert
+ expect(summary['total_readings'], equals(15));
+ expect(summary['current_streak'], equals(3));
+ expect(summary['longest_streak'], equals(10));
+ expect(summary['days_since_last_activity'], equals(2));
+ expect(summary['churn_risk_level'], isNotNull);
+ expect(summary['churn_risk_score'], isA<double>());
+ expect(summary['engagement_status'], isNotNull);
+ });
+ });
+
+ group('ChurnPredictionService - Error Handling', () {
+ test('handles error gracefully when stats service fails', () async {
+ // Arrange
+ when(() => mockStatsService.getStats())
+ .thenThrow(Exception('Database error'));
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel,
+ ChurnRiskLevel.unknown); // Error case returns unknown
+ expect(prediction.riskScore, equals(0.0));
+ expect(prediction.shouldSendNotification, false);
+ expect(prediction.reason, contains('Error'));
+ });
+
+ test('handles error gracefully when notification service fails', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 0,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 8)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+ when(() => mockNotificationService.areNotificationsEnabled())
+ .thenThrow(Exception('Permission error'));
+
+ // Act & Assert - Should not throw
+ final prediction = await churnPredictionService.predictChurnRisk();
+ expect(
+ () async => await churnPredictionService
+ .sendChurnPreventionNotification(prediction),
+ returnsNormally,
+ );
+ });
+ });
+
+ group('ChurnPredictionService - Edge Cases', () {
+ test('handles user with same current and longest streak', () async {
+ // Arrange
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 10,
+ currentStreak: 10,
+ longestStreak: 10,
+ lastActivityDate: DateTime.now(),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.riskLevel, ChurnRiskLevel.low);
+ expect(prediction.daysSinceLastActivity, equals(0));
+ });
+
+ test('handles boundary conditions for inactive days', () async {
+ // Arrange: Exactly at medium threshold
+ final stats = SpiritualStats(
+ totalDevocionalesRead: 5,
+ currentStreak: 0,
+ longestStreak: 5,
+ lastActivityDate: DateTime.now().subtract(const Duration(days: 3)),
+ );
+ when(() => mockStatsService.getStats()).thenAnswer((_) async => stats);
+
+ // Act
+ final prediction = await churnPredictionService.predictChurnRisk();
+
+ // Assert
+ expect(prediction.daysSinceLastActivity, equals(3));
+ expect(prediction.shouldSendNotification, true);
+ });
+ });
+}
----------------------------------------
📄 ARCHIVO: test/utils/churn_monitoring_helper_test.dart
Estado: added (+342/-0)
Raw URL: https://github.com/develop4God/Devocional_nuevo/raw/ed6556234f138d7e356cc1d8314363d37910a7c6/test%2Futils%2Fchurn_monitoring_helper_test.dart
DIFF:
----------------------------------------
@@ -0,0 +1,342 @@
+// test/utils/churn_monitoring_helper_test.dart
+
+import 'package:devocional_nuevo/services/churn_prediction_service.dart';
+import 'package:devocional_nuevo/services/localization_service.dart';
+import 'package:devocional_nuevo/services/notification_service.dart';
+import 'package:devocional_nuevo/services/service_locator.dart';
+import 'package:devocional_nuevo/utils/churn_monitoring_helper.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+// Mock classes
+class MockChurnPredictionService extends Mock
+ implements ChurnPredictionService {}
+
+class MockNotificationService extends Mock implements NotificationService {}
+
+class MockLocalizationService extends Mock implements LocalizationService {}
+
+// Fake class for ChurnPrediction
+class FakeChurnPrediction extends Fake implements ChurnPrediction {}
+
+void main() {
+ late MockChurnPredictionService mockChurnService;
+ late MockNotificationService mockNotificationService;
+ late MockLocalizationService mockLocalizationService;
+ late ServiceLocator serviceLocator;
+
+ setUpAll(() {
+ // Register fallback value for ChurnPrediction
+ registerFallbackValue(FakeChurnPrediction());
+ });
+
+ setUp(() async {
+ // Reset SharedPreferences
+ SharedPreferences.setMockInitialValues({});
+
+ // Reset service locator before each test
+ serviceLocator = ServiceLocator();
+ serviceLocator.reset();
+
+ mockChurnService = MockChurnPredictionService();
+ mockNotificationService = MockNotificationService();
+ mockLocalizationService = MockLocalizationService();
+
+ // Register mock services
+ serviceLocator.registerFactory<ChurnPredictionService>(
+ () => mockChurnService,
+ );
+ serviceLocator.registerFactory<LocalizationService>(
+ () => mockLocalizationService,
+ );
+
+ // Setup default localization responses
+ when(() =>
+ mockLocalizationService.translate('churn_notification.high_title'))
+ .thenReturn('We miss you! 🙏');
+ when(() => mockLocalizationService
+ .translate('churn_notification.medium_title'))
+ .thenReturn('Your devotional is waiting 📖');
+ when(() => mockLocalizationService.translate(
+ 'churn_notification.high_body', any()))
+ .thenReturn(
+ 'X days have passed. Come back and connect with your faith.');
+ when(() =>
+ mockLocalizationService.translate('churn_notification.medium_body'))
+ .thenReturn('Don\'t lose your streak. Read today\'s devotional.');
+ });
+
+ tearDown(() {
+ serviceLocator.reset();
+ });
+
+ group('ChurnMonitoringHelper - Daily Check', () {
+ test('performs daily check successfully for high risk user', () async {
+ // Arrange
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.high,
+ riskScore: 0.8,
+ daysSinceLastActivity: 8,
+ shouldSendNotification: true,
+ reason: 'High risk',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Assert
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+ });
+
+ test('skips check when notifications disabled by user', () async {
+ // Arrange
+ SharedPreferences.setMockInitialValues({
+ 'churn_notifications_enabled': false,
+ });
+
+ // Act
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Assert - Should not call prediction service
+ verifyNever(() => mockChurnService.predictChurnRisk());
+ });
+
+ test('handles error gracefully during daily check', () async {
+ // Arrange
+ when(() => mockChurnService.predictChurnRisk())
+ .thenThrow(Exception('Network error'));
+
+ // Act & Assert - Should not throw
+ expect(
+ () async => await ChurnMonitoringHelper.performDailyCheck(),
+ returnsNormally,
+ );
+ });
+ });
+
+ group('ChurnMonitoringHelper - Engagement Summary', () {
+ test('gets engagement summary successfully', () async {
+ // Arrange
+ final expectedSummary = {
+ 'total_readings': 10,
+ 'current_streak': 3,
+ 'churn_risk_level': 'ChurnRiskLevel.low',
+ };
+ when(() => mockChurnService.getEngagementSummary())
+ .thenAnswer((_) async => expectedSummary);
+
+ // Act
+ final summary = await ChurnMonitoringHelper.getEngagementSummary();
+
+ // Assert
+ expect(summary, equals(expectedSummary));
+ verify(() => mockChurnService.getEngagementSummary()).called(1);
+ });
+
+ test('handles error gracefully when getting engagement summary', () async {
+ // Arrange
+ when(() => mockChurnService.getEngagementSummary())
+ .thenThrow(Exception('Database error'));
+
+ // Act
+ final summary = await ChurnMonitoringHelper.getEngagementSummary();
+
+ // Assert
+ expect(summary, isEmpty);
+ });
+ });
+
+ group('ChurnMonitoringHelper - Risk Check', () {
+ test('checks user risk successfully', () async {
+ // Arrange
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.medium,
+ riskScore: 0.5,
+ daysSinceLastActivity: 3,
+ shouldSendNotification: true,
+ reason: 'Test reason',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ final riskLevel = await ChurnMonitoringHelper.checkUserRisk();
+
+ // Assert
+ expect(riskLevel, equals(ChurnRiskLevel.medium));
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+ });
+
+ test('returns unknown risk on error', () async {
+ // Arrange
+ when(() => mockChurnService.predictChurnRisk())
+ .thenThrow(Exception('Error'));
+
+ // Act
+ final riskLevel = await ChurnMonitoringHelper.checkUserRisk();
+
+ // Assert
+ expect(
+ riskLevel, equals(ChurnRiskLevel.unknown)); // Error returns unknown
+ });
+ });
+
+ group('ChurnMonitoringHelper - Manual Notification', () {
+ test('bypasses rate limiting for manual sends', () async {
+ // Arrange
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.high,
+ riskScore: 0.8,
+ daysSinceLastActivity: 8,
+ shouldSendNotification: true,
+ reason: 'High risk',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ await ChurnMonitoringHelper.sendChurnPreventionNotification();
+
+ // Assert
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+ });
+
+ test('does not send notification when not needed', () async {
+ // Arrange
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.low,
+ riskScore: 0.1,
+ daysSinceLastActivity: 1,
+ shouldSendNotification: false,
+ reason: 'Low risk',
+ calculatedAt: DateTime.now().toUtc(),
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ await ChurnMonitoringHelper.sendChurnPreventionNotification();
+
+ // Assert
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+ // No notification service calls expected
+ });
+
+ test('handles error gracefully when sending notification', () async {
+ // Arrange
+ when(() => mockChurnService.predictChurnRisk())
+ .thenThrow(Exception('Error'));
+
+ // Act & Assert - Should not throw
+ expect(
+ () async =>
+ await ChurnMonitoringHelper.sendChurnPreventionNotification(),
+ returnsNormally,
+ );
+ });
+ });
+
+ group('ChurnMonitoringHelper - Rate Limiting', () {
+ test('enforces 2 notifications per week', () async {
+ // Arrange: Setup 2 notifications already sent in the past 3 days
+ final now = DateTime.now().toUtc();
+ SharedPreferences.setMockInitialValues({
+ 'churn_notifications_sent': [
+ now.subtract(const Duration(days: 1)).toIso8601String(),
+ now.subtract(const Duration(days: 3)).toIso8601String(),
+ ],
+ 'churn_notifications_enabled': true,
+ });
+
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.high,
+ riskScore: 0.8,
+ daysSinceLastActivity: 8,
+ shouldSendNotification: true,
+ reason: 'High risk',
+ calculatedAt: now,
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Assert - Should call prediction but not send notification due to rate limit
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+
+ // Check that history count is still 2 (no new notification added)
+ final count = await ChurnMonitoringHelper.getNotificationHistoryCount();
+ expect(count, equals(2));
+ });
+
+ test('allows notification after 7-day window expires', () async {
+ // Arrange: Setup 2 notifications sent 8 days ago (outside window)
+ final now = DateTime.now().toUtc();
+ SharedPreferences.setMockInitialValues({
+ 'churn_notifications_sent': [
+ now.subtract(const Duration(days: 8)).toIso8601String(),
+ now.subtract(const Duration(days: 10)).toIso8601String(),
+ ],
+ 'churn_notifications_enabled': true,
+ });
+
+ final prediction = ChurnPrediction(
+ riskLevel: ChurnRiskLevel.high,
+ riskScore: 0.8,
+ daysSinceLastActivity: 8,
+ shouldSendNotification: true,
+ reason: 'High risk',
+ calculatedAt: now,
+ );
+ when(() => mockChurnService.predictChurnRisk())
+ .thenAnswer((_) async => prediction);
+
+ // Act
+ await ChurnMonitoringHelper.performDailyCheck();
+
+ // Assert
+ verify(() => mockChurnService.predictChurnRisk()).called(1);
+
+ // Old notifications should be cleaned up, count should be 1 (new notification)
+ final count = await ChurnMonitoringHelper.getNotificationHistoryCount();
+ expect(count, equals(1));
+ });
+
+ test('cleans up old notification history', () async {
+ // Arrange: Mix of old and recent notifications
+ final now = DateTime.now().toUtc();
+ SharedPreferences.setMockInitialValues({
+ 'churn_notifications_sent': [
+ now.subtract(const Duration(days: 2)).toIso8601String(),
+ now.subtract(const Duration(days: 8)).toIso8601String(),
+ now.subtract(const Duration(days: 15)).toIso8601String(),
+ ],
+ 'churn_notifications_enabled': true,
+ });
+
+ // Act - Get count which triggers cleanup
+ final count = await ChurnMonitoringHelper.getNotificationHistoryCount();
+
+ // Assert - Only 1 notification within 7-day window
+ expect(count, equals(1));
+ });
+
+ test('handles empty notification history', () async {
+ // Arrange
+ SharedPreferences.setMockInitialValues({});
+
+ // Act
+ final count = await ChurnMonitoringHelper.getNotificationHistoryCount();
+
+ // Assert
+ expect(count, equals(0));
+ });
+ });
+}
----------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment