kasy-cli 1.12.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +143 -7
- package/lib/commands/add.js +2 -2
- package/lib/commands/codemagic.js +11 -4
- package/lib/commands/deploy.js +3 -3
- package/lib/commands/favicon.js +115 -0
- package/lib/commands/icon.js +143 -0
- package/lib/commands/ios.js +20 -5
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +287 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +219 -0
- package/lib/commands/update.js +1 -1
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +1 -1
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
- package/lib/scaffold/backends/api/patch/lib/main.dart +29 -10
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/backends/firebase/tokens.js +2 -2
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
- package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
- package/lib/scaffold/backends/supabase/patch/README.md +1 -1
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +29 -10
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/features/README.md +15 -139
- package/lib/scaffold/shared/generator-utils.js +16 -15
- package/lib/utils/apple-release.js +85 -16
- package/lib/utils/checks.js +4 -105
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/i18n.js +413 -0
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +2 -3
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/android/app/build.gradle.kts +10 -1
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +160 -11
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/launch_background.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/launch_background.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +46 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +2 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
- package/templates/firebase/assets/images/favicon.png +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light.png +0 -0
- package/templates/firebase/firestore.indexes.json +10 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
- package/templates/firebase/functions/src/index.ts +1 -0
- package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
- package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +9 -8
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +33 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
- package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
- package/templates/firebase/ios/Runner/Info.plist +2 -2
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +67 -19
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
- package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
- package/templates/firebase/lib/core/theme/providers/theme_provider.dart +48 -24
- package/templates/firebase/lib/features/home/home_page.dart +0 -6
- package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +4 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +1 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +13 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +158 -18
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
- package/templates/firebase/lib/i18n/en.i18n.json +10 -3
- package/templates/firebase/lib/i18n/es.i18n.json +10 -3
- package/templates/firebase/lib/i18n/pt.i18n.json +10 -3
- package/templates/firebase/lib/main.dart +29 -10
- package/templates/firebase/pubspec.yaml +10 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +1 -1
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +50 -39
- package/templates/firebase/web/manifest.json +3 -3
- package/templates/firebase/web/splash/img/dark-1x.png +0 -0
- package/templates/firebase/web/splash/img/dark-2x.png +0 -0
- package/templates/firebase/web/splash/img/dark-3x.png +0 -0
- package/templates/firebase/web/splash/img/dark-4x.png +0 -0
- package/templates/firebase/web/splash/img/light-1x.png +0 -0
- package/templates/firebase/web/splash/img/light-2x.png +0 -0
- package/templates/firebase/web/splash/img/light-3x.png +0 -0
- package/templates/firebase/web/splash/img/light-4x.png +0 -0
- package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +0 -124
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +0 -12
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +0 -12
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +0 -12
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +0 -17
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +0 -17
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +0 -17
- package/lib/scaffold/features/ci/.github/dependabot.yml +0 -16
- package/lib/scaffold/features/ci/.github/workflows/app.yml +0 -20
- package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +0 -14
- package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +0 -19
- package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +0 -163
- package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +0 -28
- package/lib/scaffold/features/ci/.gitlab-ci.yml +0 -37
- package/lib/scaffold/features/ci/codemagic.yaml +0 -157
- package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +0 -111
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +0 -27
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +0 -27
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +0 -50
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +0 -79
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +0 -48
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +0 -42
- package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +0 -147
- package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +0 -95
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +0 -175
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -76
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +0 -279
- package/lib/scaffold/features/ios-release/.kasy/apple.env.example +0 -8
- package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +0 -7
- package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +0 -50
- package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +0 -50
- package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +0 -50
- package/lib/scaffold/features/ios-release/docs/ios-release.en.md +0 -41
- package/lib/scaffold/features/ios-release/docs/ios-release.es.md +0 -41
- package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +0 -41
- package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +0 -38
- package/lib/scaffold/features/ios-release/scripts/release-ios.sh +0 -137
- package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +0 -301
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +0 -81
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +0 -76
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +0 -282
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +0 -24
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +0 -71
- package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +0 -92
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +0 -15
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +0 -78
- package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +0 -29
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +0 -30
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -66
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +0 -72
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +0 -92
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +0 -89
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +0 -94
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +0 -80
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +0 -139
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +0 -110
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +0 -84
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -173
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +0 -45
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +0 -77
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +0 -392
- package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +0 -116
- package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +0 -322
- package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +0 -41
- package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +0 -305
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
- package/templates/firebase/assets/images/splashscreen.png +0 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<string>pt-BR</string>
|
|
19
19
|
</array>
|
|
20
20
|
<key>CFBundleDisplayName</key>
|
|
21
|
-
<string>
|
|
21
|
+
<string>Kasy App</string>
|
|
22
22
|
<key>CFBundleExecutable</key>
|
|
23
23
|
<string>$(EXECUTABLE_NAME)</string>
|
|
24
24
|
<key>CFBundleIdentifier</key>
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
<key>FacebookClientToken</key>
|
|
58
58
|
<string>00000000000000000000000000000000</string>
|
|
59
59
|
<key>FacebookDisplayName</key>
|
|
60
|
-
<string>
|
|
60
|
+
<string>Kasy App</string>
|
|
61
61
|
<key>LSRequiresIPhoneOS</key>
|
|
62
62
|
<true/>
|
|
63
63
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Localized Info.plist strings — es resources for system picker/camera UI (iOS). */
|
|
2
|
-
"CFBundleDisplayName" = "
|
|
2
|
+
"CFBundleDisplayName" = "Kasy App";
|
|
3
3
|
"NSCameraUsageDescription" = "Necesitamos la cámara para tomar fotos y vídeos.";
|
|
4
4
|
"NSPhotoLibraryAddUsageDescription" = "Necesitamos acceso para guardar fotos y vídeos en la galería.";
|
|
5
5
|
"NSPhotoLibraryUsageDescription" = "Necesitamos acceso para mostrar tus fotos recientes y abrir la galería desde la cámara.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Localized Info.plist strings — presence of pt-BR resources lets iOS use Portuguese for system UI (e.g. image picker). */
|
|
2
|
-
"CFBundleDisplayName" = "
|
|
2
|
+
"CFBundleDisplayName" = "Kasy App";
|
|
3
3
|
"NSCameraUsageDescription" = "Precisamos da câmera para tirar fotos e vídeos.";
|
|
4
4
|
"NSPhotoLibraryAddUsageDescription" = "Precisamos de acesso para salvar fotos e vídeos na galeria.";
|
|
5
5
|
"NSPhotoLibraryUsageDescription" = "Precisamos de acesso para mostrar suas fotos recentes e abrir a galeria a partir da câmera.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Localized Info.plist strings — presence of pt-BR resources lets iOS use Portuguese for system UI (e.g. image picker). */
|
|
2
|
-
"CFBundleDisplayName" = "
|
|
2
|
+
"CFBundleDisplayName" = "Kasy App";
|
|
3
3
|
"NSCameraUsageDescription" = "Precisamos da câmera para tirar fotos e vídeos.";
|
|
4
4
|
"NSPhotoLibraryAddUsageDescription" = "Precisamos de acesso para salvar fotos e vídeos na galeria.";
|
|
5
5
|
"NSPhotoLibraryUsageDescription" = "Precisamos de acesso para mostrar suas fotos recentes e abrir a galeria a partir da câmera.";
|
|
@@ -377,6 +377,9 @@ class KasyButton extends StatelessWidget {
|
|
|
377
377
|
}
|
|
378
378
|
final KasyColors c = context.colors;
|
|
379
379
|
final Color soft = c.surfaceNeutralSoft;
|
|
380
|
+
// Variants used on non-theme-matched backgrounds (e.g. inverse on the paywall gradient)
|
|
381
|
+
// need an explicit case — the generic disabled fallback blends with surfaceNeutralSoft,
|
|
382
|
+
// which goes near-black in dark mode and kills contrast on colored surfaces.
|
|
380
383
|
return switch (variant) {
|
|
381
384
|
KasyButtonVariant.primary => _KasyButtonPalette(
|
|
382
385
|
background: Color.alphaBlend(c.primary.withValues(alpha: 0.62), soft),
|
|
@@ -388,6 +391,11 @@ class KasyButton extends StatelessWidget {
|
|
|
388
391
|
foreground: c.primary.withValues(alpha: 0.90),
|
|
389
392
|
border: Colors.transparent,
|
|
390
393
|
),
|
|
394
|
+
KasyButtonVariant.inverse => _KasyButtonPalette(
|
|
395
|
+
background: c.onPrimary,
|
|
396
|
+
foreground: c.primary.withValues(alpha: 0.62),
|
|
397
|
+
border: Colors.transparent,
|
|
398
|
+
),
|
|
391
399
|
_ => null,
|
|
392
400
|
};
|
|
393
401
|
}
|
|
@@ -96,11 +96,29 @@ final class KasyPaddedSurfaceBottomBarFactory extends BartBottomBarFactory {
|
|
|
96
96
|
),
|
|
97
97
|
Theme(
|
|
98
98
|
data: Theme.of(context).copyWith(
|
|
99
|
+
splashFactory: NoSplash.splashFactory,
|
|
100
|
+
splashColor: Colors.transparent,
|
|
101
|
+
highlightColor: Colors.transparent,
|
|
99
102
|
navigationBarTheme: NavigationBarTheme.of(context).copyWith(
|
|
100
103
|
backgroundColor: Colors.transparent,
|
|
101
104
|
elevation: 0,
|
|
102
105
|
shadowColor: Colors.transparent,
|
|
103
106
|
surfaceTintColor: Colors.transparent,
|
|
107
|
+
indicatorColor: Colors.transparent,
|
|
108
|
+
iconTheme: WidgetStateProperty.resolveWith((states) {
|
|
109
|
+
final selected = states.contains(WidgetState.selected);
|
|
110
|
+
return IconThemeData(
|
|
111
|
+
color: selected ? colors.primary : colors.muted,
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
|
115
|
+
final selected = states.contains(WidgetState.selected);
|
|
116
|
+
final base = Theme.of(context).textTheme.labelMedium ??
|
|
117
|
+
const TextStyle();
|
|
118
|
+
return base.copyWith(
|
|
119
|
+
color: selected ? colors.primary : colors.muted,
|
|
120
|
+
);
|
|
121
|
+
}),
|
|
104
122
|
),
|
|
105
123
|
),
|
|
106
124
|
child: BartMaterial3BottomBar(
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:io' show Platform;
|
|
3
|
+
|
|
4
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
1
5
|
import 'package:home_widget/home_widget.dart';
|
|
2
6
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
3
7
|
import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
|
|
4
|
-
import 'package:kasy_kit/core/states/translations.dart';
|
|
5
8
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
6
9
|
import 'package:kasy_kit/features/subscription/repositories/subscription_repository.dart';
|
|
7
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -19,10 +22,11 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
19
22
|
@override
|
|
20
23
|
void build() {
|
|
21
24
|
// Auto-refresh the widget whenever user state changes in a way that
|
|
22
|
-
// affects what it renders (login/logout, name, premium status).
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
25
|
+
// affects what it renders (login/logout, name, email, premium status).
|
|
26
|
+
// The initial render is triggered explicitly by HomeWidgetsManager.init()
|
|
27
|
+
// after setAppGroupId completes — putting it here would race with the
|
|
28
|
+
// app-group setup and the first saveWidgetData could land in the wrong
|
|
29
|
+
// UserDefaults suite.
|
|
26
30
|
ref.listen(userStateNotifierProvider, (previous, next) {
|
|
27
31
|
if (previous == null) return;
|
|
28
32
|
if (_widgetSignature(previous.user) != _widgetSignature(next.user)) {
|
|
@@ -33,7 +37,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
33
37
|
|
|
34
38
|
/// Snapshot of the user fields the widget reads. Used to skip the update
|
|
35
39
|
/// when an unrelated field changes (e.g. lastUpdateDate refresh).
|
|
36
|
-
(String?, String?, bool) _widgetSignature(User user) {
|
|
40
|
+
(String?, String?, String?, bool) _widgetSignature(User user) {
|
|
37
41
|
final isPro = switch (user) {
|
|
38
42
|
AuthenticatedUserData(:final subscription) ||
|
|
39
43
|
AnonymousUserData(:final subscription) =>
|
|
@@ -44,34 +48,62 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
44
48
|
AuthenticatedUserData(:final name) => name,
|
|
45
49
|
_ => null,
|
|
46
50
|
};
|
|
47
|
-
|
|
51
|
+
final email = switch (user) {
|
|
52
|
+
AuthenticatedUserData(:final email) => email,
|
|
53
|
+
_ => null,
|
|
54
|
+
};
|
|
55
|
+
return (user.idOrNull, name, email, isPro);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
@override
|
|
51
|
-
Future<void> update()
|
|
59
|
+
Future<void> update() => updateForLocale(LocaleSettings.currentLocale);
|
|
60
|
+
|
|
61
|
+
/// Same as [update] but renders against an explicit locale. Use this
|
|
62
|
+
/// from the language picker so the widget never falls one step behind:
|
|
63
|
+
/// `LocaleSettings.setLocale` propagates to `currentLocale` over a
|
|
64
|
+
/// frame boundary, and an [update] call scheduled at the same time
|
|
65
|
+
/// can race with it. Passing the locale removes the race.
|
|
66
|
+
Future<void> updateForLocale(AppLocale locale) async {
|
|
52
67
|
final logger = Logger();
|
|
53
|
-
logger.i('🔄 Updating MyWidget Home Widget');
|
|
68
|
+
logger.i('🔄 Updating MyWidget Home Widget (${locale.languageCode})');
|
|
54
69
|
final user = ref.read(userStateNotifierProvider).user;
|
|
55
|
-
final t =
|
|
70
|
+
final t = locale.translations;
|
|
71
|
+
|
|
72
|
+
// "Logged out" = no user id at all (post-logout in authRequired mode, or
|
|
73
|
+
// before any anonymous signup completes). In this state we show a
|
|
74
|
+
// come-back message and hide the plan tag — showing a plan would be
|
|
75
|
+
// misleading when there is no account behind it.
|
|
76
|
+
final isLoggedOut = user.idOrNull == null;
|
|
56
77
|
|
|
57
78
|
final name = switch (user) {
|
|
58
79
|
AuthenticatedUserData(:final name)
|
|
59
80
|
when name != null && name.isNotEmpty =>
|
|
60
81
|
name.split(' ').first,
|
|
82
|
+
// Fallback when the Firestore profile has no name yet: derive a
|
|
83
|
+
// display name from the email local-part (matches what the
|
|
84
|
+
// settings page shows).
|
|
85
|
+
AuthenticatedUserData(:final email) => email.split('@').first,
|
|
61
86
|
_ => null,
|
|
62
87
|
};
|
|
63
88
|
|
|
64
|
-
final isPro = await _resolveIsPro(user);
|
|
89
|
+
final isPro = !isLoggedOut && await _resolveIsPro(user);
|
|
65
90
|
|
|
66
91
|
final greeting = _greeting(t);
|
|
67
|
-
final title =
|
|
68
|
-
? t.home_widget.
|
|
69
|
-
:
|
|
70
|
-
|
|
92
|
+
final title = isLoggedOut
|
|
93
|
+
? t.home_widget.title_logged_out
|
|
94
|
+
: name == null
|
|
95
|
+
? t.home_widget.title_default
|
|
96
|
+
: t.home_widget.title_with_name(name: name);
|
|
97
|
+
// Empty planText is the contract used by the native widget to skip
|
|
98
|
+
// rendering the pill — see MyWidget.swift / MyWidget.kt.
|
|
99
|
+
final planText = isLoggedOut
|
|
100
|
+
? ''
|
|
101
|
+
: (isPro ? t.home_widget.plan_pro : t.home_widget.plan_free);
|
|
102
|
+
final quote = t.home_widget.quote;
|
|
71
103
|
|
|
72
104
|
logger.d(
|
|
73
105
|
'Widget payload → greeting: "$greeting", title: "$title", '
|
|
74
|
-
'planText: "$planText", isPro: $isPro',
|
|
106
|
+
'planText: "$planText", isPro: $isPro, loggedOut: $isLoggedOut',
|
|
75
107
|
);
|
|
76
108
|
|
|
77
109
|
return updateWidget({
|
|
@@ -79,6 +111,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
79
111
|
'title': title,
|
|
80
112
|
'planText': planText,
|
|
81
113
|
'isPro': isPro.toString(),
|
|
114
|
+
'quote': quote,
|
|
82
115
|
});
|
|
83
116
|
}
|
|
84
117
|
|
|
@@ -98,9 +131,14 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
98
131
|
if (userId == null) return cached;
|
|
99
132
|
try {
|
|
100
133
|
final repo = ref.read(subscriptionRepositoryProvider);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
// 2s timeout — if RevenueCat/network is slow, fall back to the cached
|
|
135
|
+
// value so the widget renders promptly on first install. The next
|
|
136
|
+
// background tick (or any user-state change) will reconcile later.
|
|
137
|
+
return await Future(() async {
|
|
138
|
+
await repo.initUser(userId);
|
|
139
|
+
final fresh = await repo.get(userId);
|
|
140
|
+
return fresh.isActive;
|
|
141
|
+
}).timeout(const Duration(seconds: 2), onTimeout: () => cached);
|
|
104
142
|
} catch (e) {
|
|
105
143
|
Logger().w('Widget could not refresh subscription: $e (using cached)');
|
|
106
144
|
return cached;
|
|
@@ -112,6 +150,16 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
112
150
|
await HomeWidget.saveWidgetData<String>('title', data['title'] ?? '');
|
|
113
151
|
await HomeWidget.saveWidgetData<String>('planText', data['planText'] ?? '');
|
|
114
152
|
await HomeWidget.saveWidgetData<String>('isPro', data['isPro'] ?? 'false');
|
|
153
|
+
await HomeWidget.saveWidgetData<String>('quote', data['quote'] ?? '');
|
|
154
|
+
|
|
155
|
+
// On Android, saveWidgetData writes to SharedPreferences asynchronously.
|
|
156
|
+
// Glance's HomeWidgetGlanceStateDefinition reads from the same prefs, but
|
|
157
|
+
// a tight saveWidgetData→updateWidget sequence can race with the commit —
|
|
158
|
+
// Glance occasionally recomposes with the previous values (most visible
|
|
159
|
+
// right after a locale change). A small yield lets the writes settle.
|
|
160
|
+
if (!kIsWeb && Platform.isAndroid) {
|
|
161
|
+
await Future<void>.delayed(const Duration(milliseconds: 120));
|
|
162
|
+
}
|
|
115
163
|
|
|
116
164
|
await HomeWidget.updateWidget(
|
|
117
165
|
name: _androidWidgetName,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
1
3
|
import 'package:background_fetch/background_fetch.dart';
|
|
2
4
|
import 'package:flutter/foundation.dart';
|
|
3
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
@@ -8,14 +10,7 @@ import 'package:kasy_kit/core/initializer/onstart_service.dart';
|
|
|
8
10
|
import 'package:logger/logger.dart';
|
|
9
11
|
|
|
10
12
|
final homeWidgetsManagerProvider = Provider<HomeWidgetsManager>(
|
|
11
|
-
(ref)
|
|
12
|
-
// Force-build the widget service at app startup so its user-state
|
|
13
|
-
// listener attaches and the widget auto-refreshes when subscription
|
|
14
|
-
// status or other relevant fields change. Without this read, the
|
|
15
|
-
// listener would only attach on first manual update.
|
|
16
|
-
ref.read(myWidgetHomeWidgetProvider.notifier);
|
|
17
|
-
return HomeWidgetsManager();
|
|
18
|
-
},
|
|
13
|
+
(ref) => HomeWidgetsManager(ref),
|
|
19
14
|
);
|
|
20
15
|
|
|
21
16
|
const String appGroupId = 'group.com.aicrus.firebase.kit';
|
|
@@ -26,12 +21,31 @@ const String appGroupId = 'group.com.aicrus.firebase.kit';
|
|
|
26
21
|
/// will be used to initialize the home widgets and set the app group id
|
|
27
22
|
/// Register the background task for the home widgets
|
|
28
23
|
class HomeWidgetsManager implements OnStartService {
|
|
24
|
+
HomeWidgetsManager(this._ref);
|
|
25
|
+
|
|
26
|
+
final Ref _ref;
|
|
27
|
+
|
|
29
28
|
@override
|
|
30
29
|
Future<void> init() async {
|
|
31
30
|
if (kIsWeb) return;
|
|
32
31
|
try {
|
|
32
|
+
// Must be set BEFORE any saveWidgetData call, otherwise the data lands
|
|
33
|
+
// in the default UserDefaults suite and the native widget extension
|
|
34
|
+
// (which reads from the app group) sees nothing.
|
|
33
35
|
await HomeWidget.setAppGroupId(appGroupId);
|
|
34
36
|
|
|
37
|
+
// Read the widget notifier so its user-state listener attaches —
|
|
38
|
+
// future state changes (login/logout, subscription) auto-refresh
|
|
39
|
+
// the widget without waiting for the 15-min background tick.
|
|
40
|
+
final myWidget = _ref.read(myWidgetHomeWidgetProvider.notifier);
|
|
41
|
+
// Push initial data so the widget renders something on first install
|
|
42
|
+
// instead of staying blank until the background task fires.
|
|
43
|
+
// Fire-and-forget: we do NOT await here because update() may do a
|
|
44
|
+
// network call (RevenueCat) that could stall app startup and even
|
|
45
|
+
// prevent BackgroundFetch from being configured. setAppGroupId has
|
|
46
|
+
// already completed, so it is safe to fire it off now.
|
|
47
|
+
unawaited(myWidget.update());
|
|
48
|
+
|
|
35
49
|
final status = await BackgroundFetch.configure(
|
|
36
50
|
BackgroundFetchConfig(
|
|
37
51
|
minimumFetchInterval: 15,
|
|
@@ -60,11 +60,23 @@ class SharedPreferencesBuilder implements OnStartService {
|
|
|
60
60
|
return prefs.getBool('biometric_enabled') ?? false;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/// How many times the user dismissed the ATT soft prompt without accepting.
|
|
64
|
+
/// Used to back off and eventually stop asking. See [MaybeShowAttPermission].
|
|
65
|
+
int getAttSoftDismissCount() {
|
|
66
|
+
return prefs.getInt('att_soft_dismiss_count') ?? 0;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
Future<void> setAttSoftDismissCount(int count) async {
|
|
70
|
+
await prefs.setInt('att_soft_dismiss_count', count);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
DateTime? getAttSoftLastAskedAt() {
|
|
74
|
+
final millis = prefs.getInt('att_soft_last_asked_at');
|
|
75
|
+
if (millis == null) return null;
|
|
76
|
+
return DateTime.fromMillisecondsSinceEpoch(millis);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Future<void> setAttSoftLastAskedAt(DateTime when) async {
|
|
80
|
+
await prefs.setInt('att_soft_last_asked_at', when.millisecondsSinceEpoch);
|
|
69
81
|
}
|
|
70
82
|
}
|
|
@@ -13,20 +13,16 @@ import 'package:kasy_kit/core/states/models/event_model.dart';
|
|
|
13
13
|
/// Ex of usage:
|
|
14
14
|
/// @override
|
|
15
15
|
/// Widget build(BuildContext context) {
|
|
16
|
-
/// final homeState = ref.watch(homeNotifierProvider);
|
|
17
|
-
/// final userState = ref.watch(userStateNotifierProvider);
|
|
18
|
-
/// // final translations = ref.watch(translationsProvider);
|
|
19
|
-
///
|
|
20
16
|
/// return ConditionalWidgetsEvents(
|
|
21
17
|
/// eventWidgets: [
|
|
22
18
|
/// MaybeShowPremiumPage(),
|
|
23
|
-
///
|
|
24
|
-
/// MaybeLevelUpBottomSheet(),
|
|
19
|
+
/// MaybeShowAttPermission(),
|
|
25
20
|
/// MaybeAskForReview(),
|
|
26
21
|
/// MaybeAskForRating(),
|
|
27
22
|
/// ],
|
|
28
|
-
/// child:
|
|
29
|
-
///
|
|
23
|
+
/// child: ...,
|
|
24
|
+
/// );
|
|
25
|
+
/// }
|
|
30
26
|
/// A widget that can be shown or not based on a condition.
|
|
31
27
|
sealed class MaybeShow {}
|
|
32
28
|
|
|
@@ -6,6 +6,7 @@ import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
7
7
|
import 'package:kasy_kit/core/data/repositories/user_repository.dart';
|
|
8
8
|
import 'package:kasy_kit/core/initializer/onstart_service.dart';
|
|
9
|
+
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
9
10
|
import 'package:kasy_kit/core/states/models/user_state.dart';
|
|
10
11
|
import 'package:kasy_kit/environnements.dart';
|
|
11
12
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
@@ -125,8 +126,19 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
125
126
|
Future<void> onLogout() async {
|
|
126
127
|
final userId = state.user.idOrThrow;
|
|
127
128
|
_deviceRepository.removeTokenUpdateListener();
|
|
128
|
-
|
|
129
|
+
// Best-effort: if the network call fails we still proceed with logout so
|
|
130
|
+
// the user is never stuck on the previous account. A stale device doc on
|
|
131
|
+
// the old user is cleaned up server-side by the cross-user token dedup
|
|
132
|
+
// trigger when the same install registers under a new account.
|
|
133
|
+
try {
|
|
134
|
+
await _deviceRepository.unregister(userId);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
_logger.w('Failed to unregister device during logout: $e');
|
|
137
|
+
}
|
|
129
138
|
await _authenticationRepository.logout();
|
|
139
|
+
// Biometric lock is a per-account preference, not a device-wide one.
|
|
140
|
+
// The next user signing in on this install should start without it set.
|
|
141
|
+
await ref.read(sharedPreferencesProvider).setBiometricEnabled(false);
|
|
130
142
|
state = const UserState(user: User.anonymous());
|
|
131
143
|
if (mode == AuthenticationMode.anonymous) {
|
|
132
144
|
await _loadAnonymousState();
|
|
@@ -17,13 +17,8 @@ class ThemeProvider extends InheritedNotifier<AppTheme> {
|
|
|
17
17
|
|
|
18
18
|
@override
|
|
19
19
|
bool updateShouldNotify(covariant InheritedNotifier<AppTheme> oldWidget) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// keeep the same theme when switching between light and dark mode while hot reloading
|
|
23
|
-
notifier!.mode = oldWidget.notifier!.mode;
|
|
24
|
-
notifier!.setSystemBarColor();
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
20
|
+
return oldWidget.notifier != notifier ||
|
|
21
|
+
oldWidget.notifier?.mode != notifier?.mode;
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
static AppTheme of(BuildContext context) =>
|
|
@@ -37,7 +32,7 @@ class ThemeProvider extends InheritedNotifier<AppTheme> {
|
|
|
37
32
|
///
|
|
38
33
|
/// Defining a theme for light and dark should only change the colors
|
|
39
34
|
/// not redefining everything. (see ./docs/theme.md)
|
|
40
|
-
class AppTheme with ChangeNotifier {
|
|
35
|
+
class AppTheme with ChangeNotifier, WidgetsBindingObserver {
|
|
41
36
|
final KasyTheme? lightTheme;
|
|
42
37
|
final KasyTheme? darkTheme;
|
|
43
38
|
final SharedPreferences sharedPreferences;
|
|
@@ -50,6 +45,7 @@ class AppTheme with ChangeNotifier {
|
|
|
50
45
|
this.darkTheme,
|
|
51
46
|
}) {
|
|
52
47
|
mode = _loadFromPrefs();
|
|
48
|
+
WidgetsBinding.instance.addObserver(this);
|
|
53
49
|
setSystemBarColor();
|
|
54
50
|
}
|
|
55
51
|
|
|
@@ -135,25 +131,55 @@ class AppTheme with ChangeNotifier {
|
|
|
135
131
|
);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
@override
|
|
135
|
+
void didChangePlatformBrightness() {
|
|
136
|
+
super.didChangePlatformBrightness();
|
|
137
|
+
if (mode == ThemeMode.system) {
|
|
138
|
+
setSystemBarColor();
|
|
139
|
+
notifyListeners();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
void dispose() {
|
|
145
|
+
WidgetsBinding.instance.removeObserver(this);
|
|
146
|
+
super.dispose();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Resolves [mode] to either light or dark by reading the system brightness
|
|
150
|
+
/// when [mode] is [ThemeMode.system].
|
|
151
|
+
ThemeMode get effectiveMode {
|
|
152
|
+
if (mode != ThemeMode.system) return mode;
|
|
153
|
+
final brightness =
|
|
154
|
+
WidgetsBinding.instance.platformDispatcher.platformBrightness;
|
|
155
|
+
return brightness == Brightness.dark ? ThemeMode.dark : ThemeMode.light;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Set the theme mode and persist the preference.
|
|
159
|
+
void setMode(ThemeMode newMode) {
|
|
160
|
+
if (mode == newMode) return;
|
|
161
|
+
mode = newMode;
|
|
142
162
|
_saveInPrefs(mode);
|
|
143
|
-
notifyListeners();
|
|
144
163
|
setSystemBarColor();
|
|
164
|
+
notifyListeners();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Toggle between light and dark based on the effective (visible) mode.
|
|
168
|
+
/// If the user is in system mode, this picks the opposite of the
|
|
169
|
+
/// currently displayed brightness and switches out of system mode.
|
|
170
|
+
void toggle() {
|
|
171
|
+
final next =
|
|
172
|
+
effectiveMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
|
|
173
|
+
setMode(next);
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
void setSystemBarColor() {
|
|
177
|
+
final isLight = effectiveMode == ThemeMode.light;
|
|
148
178
|
SystemChrome.setSystemUIOverlayStyle(
|
|
149
179
|
SystemUiOverlayStyle(
|
|
150
180
|
statusBarColor: Colors.transparent,
|
|
151
|
-
statusBarBrightness:
|
|
152
|
-
|
|
153
|
-
statusBarIconBrightness:
|
|
154
|
-
mode == ThemeMode.light ? Brightness.dark : Brightness.light,
|
|
155
|
-
// statusBarColor: Colors.black, // color for android
|
|
156
|
-
// statusBarBrightness: Brightness.light, // for ios Dark = white status
|
|
181
|
+
statusBarBrightness: isLight ? Brightness.light : Brightness.dark,
|
|
182
|
+
statusBarIconBrightness: isLight ? Brightness.dark : Brightness.light,
|
|
157
183
|
),
|
|
158
184
|
);
|
|
159
185
|
}
|
|
@@ -189,11 +215,7 @@ class AppTheme with ChangeNotifier {
|
|
|
189
215
|
ThemeData get darkThemeData => darkTheme!.data.materialTheme;
|
|
190
216
|
|
|
191
217
|
KasyTheme get current {
|
|
192
|
-
|
|
193
|
-
return lightTheme!;
|
|
194
|
-
} else {
|
|
195
|
-
return darkTheme!;
|
|
196
|
-
}
|
|
218
|
+
return effectiveMode == ThemeMode.dark ? darkTheme! : lightTheme!;
|
|
197
219
|
}
|
|
198
220
|
|
|
199
221
|
ThemeMode _loadFromPrefs() {
|
|
@@ -202,6 +224,8 @@ class AppTheme with ChangeNotifier {
|
|
|
202
224
|
return ThemeMode.dark;
|
|
203
225
|
} else if (themeMode == ThemeMode.light.name) {
|
|
204
226
|
return ThemeMode.light;
|
|
227
|
+
} else if (themeMode == ThemeMode.system.name) {
|
|
228
|
+
return ThemeMode.system;
|
|
205
229
|
}
|
|
206
230
|
return mode;
|
|
207
231
|
}
|
|
@@ -4,7 +4,6 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
|
4
4
|
import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
|
|
5
5
|
import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
|
|
6
6
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
7
|
-
import 'package:kasy_kit/core/states/components/maybe_ask_biometric_setup.dart';
|
|
8
7
|
import 'package:kasy_kit/core/states/components/maybe_ask_rating.dart';
|
|
9
8
|
import 'package:kasy_kit/core/states/components/maybe_show_update_bottom_sheet.dart';
|
|
10
9
|
import 'package:kasy_kit/core/states/components/maybeshow_component.dart';
|
|
@@ -12,7 +11,6 @@ import 'package:kasy_kit/core/theme/theme.dart';
|
|
|
12
11
|
import 'package:kasy_kit/features/home/home_components_page.dart';
|
|
13
12
|
import 'package:kasy_kit/features/home/home_features_page.dart';
|
|
14
13
|
import 'package:kasy_kit/features/notifications/shared/att_permission.dart';
|
|
15
|
-
import 'package:kasy_kit/features/notifications/shared/notification_permission_bottom_sheet.dart';
|
|
16
14
|
import 'package:kasy_kit/features/subscription/shared/maybeshow_premium.dart';
|
|
17
15
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
18
16
|
|
|
@@ -27,13 +25,9 @@ class HomePage extends ConsumerWidget {
|
|
|
27
25
|
eventWidgets: [
|
|
28
26
|
MaybeAskForReview(),
|
|
29
27
|
MaybeAskForRating(),
|
|
30
|
-
// First OnAppStart handler that may return true — premium / notifications
|
|
31
|
-
// no longer suppress the biometric one-shot prompt.
|
|
32
|
-
MaybeAskBiometricSetup(),
|
|
33
28
|
MaybeShowPremiumPage(),
|
|
34
29
|
MaybeShowUpdateBottomSheet(),
|
|
35
30
|
MaybeShowAttPermission(),
|
|
36
|
-
MaybeShowNotificationPermission(),
|
|
37
31
|
],
|
|
38
32
|
child: ColoredBox(
|
|
39
33
|
color: context.colors.background,
|
|
@@ -38,6 +38,15 @@ abstract class DeviceApi {
|
|
|
38
38
|
/// Unregister the device in the backend
|
|
39
39
|
Future<void> unregister(String userId, String deviceId);
|
|
40
40
|
|
|
41
|
+
/// Heartbeat — update the `lastUpdateDate` on the current device doc.
|
|
42
|
+
/// Used so the backend can detect orphaned device docs from previous installs.
|
|
43
|
+
Future<void> touch(String userId, String installationId);
|
|
44
|
+
|
|
45
|
+
/// Delete device docs of the same user that haven't been touched in a while.
|
|
46
|
+
/// Called after registering a fresh installation to remove orphans left by
|
|
47
|
+
/// previous installs (whose installationId no longer matches).
|
|
48
|
+
Future<void> cleanupStaleDevices(String userId, String currentInstallationId);
|
|
49
|
+
|
|
41
50
|
/// Listen to token refresh
|
|
42
51
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh);
|
|
43
52
|
|
|
@@ -167,6 +176,54 @@ class FirebaseDeviceApi implements DeviceApi {
|
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
@override
|
|
180
|
+
Future<void> touch(String userId, String installationId) async {
|
|
181
|
+
try {
|
|
182
|
+
await retryOnFirestoreUnavailable(
|
|
183
|
+
() => _client
|
|
184
|
+
.collection('users')
|
|
185
|
+
.doc(userId)
|
|
186
|
+
.collection('devices')
|
|
187
|
+
.doc(installationId)
|
|
188
|
+
.update({'lastUpdateDate': Timestamp.now()}),
|
|
189
|
+
);
|
|
190
|
+
} catch (_) {
|
|
191
|
+
// Missing doc — happens if the device was unregistered or never saved.
|
|
192
|
+
// Caller (DeviceRepository) recovers by re-registering on next session.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@override
|
|
197
|
+
Future<void> cleanupStaleDevices(
|
|
198
|
+
String userId,
|
|
199
|
+
String currentInstallationId,
|
|
200
|
+
) async {
|
|
201
|
+
// Devices not touched in the last 30 days are treated as orphans from
|
|
202
|
+
// previous installations on the same physical device. Real second devices
|
|
203
|
+
// that the user actively uses stay above this threshold via heartbeat.
|
|
204
|
+
final cutoff = DateTime.now().subtract(const Duration(days: 30));
|
|
205
|
+
try {
|
|
206
|
+
final snapshot = await _client
|
|
207
|
+
.collection('users')
|
|
208
|
+
.doc(userId)
|
|
209
|
+
.collection('devices')
|
|
210
|
+
.where('lastUpdateDate', isLessThan: Timestamp.fromDate(cutoff))
|
|
211
|
+
.get();
|
|
212
|
+
final batch = _client.batch();
|
|
213
|
+
var hasDeletions = false;
|
|
214
|
+
for (final doc in snapshot.docs) {
|
|
215
|
+
if (doc.id == currentInstallationId) continue;
|
|
216
|
+
batch.delete(doc.reference);
|
|
217
|
+
hasDeletions = true;
|
|
218
|
+
}
|
|
219
|
+
if (hasDeletions) {
|
|
220
|
+
await batch.commit();
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
Logger().w('cleanupStaleDevices failed: $e');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
170
227
|
@override
|
|
171
228
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh) {
|
|
172
229
|
_onTokenRefreshSubscription =
|
|
@@ -117,6 +117,8 @@ sealed class NotificationPermission {
|
|
|
117
117
|
await permission.ask();
|
|
118
118
|
case NotificationPermissionDenied():
|
|
119
119
|
await permission.ask();
|
|
120
|
+
case NotificationPermissionPermanentlyDenied():
|
|
121
|
+
await permission.openSettings();
|
|
120
122
|
case NotificationPermissionGranted():
|
|
121
123
|
await permission.ensureSetup();
|
|
122
124
|
}
|
|
@@ -151,7 +153,7 @@ class NotificationPermissionGranted extends NotificationPermission {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
/// we asked for permission and it was denied
|
|
156
|
+
/// we asked for permission and it was denied (but can still be asked again)
|
|
155
157
|
class NotificationPermissionDenied extends NotificationPermission {
|
|
156
158
|
final NotificationSettings? _notificationSettings;
|
|
157
159
|
final NotificationsRepository? _repository;
|
|
@@ -175,6 +177,14 @@ class NotificationPermissionDenied extends NotificationPermission {
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/// User denied the permission and the OS will not show the native prompt again.
|
|
181
|
+
/// The only way back is the system settings of the app.
|
|
182
|
+
class NotificationPermissionPermanentlyDenied extends NotificationPermission {
|
|
183
|
+
Future<void> openSettings() async {
|
|
184
|
+
await openAppSettings();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
/// we never asked for permission
|
|
179
189
|
class NotificationPermissionWaiting extends NotificationPermission {
|
|
180
190
|
final NotificationSettings? _notificationSettings;
|