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
|
@@ -29,6 +29,14 @@ abstract class DeviceApi {
|
|
|
29
29
|
/// Register the device in the backend
|
|
30
30
|
/// Of course your backend should check if the device is already registered
|
|
31
31
|
/// throws an [ApiError] if something goes wrong
|
|
32
|
+
///
|
|
33
|
+
/// IMPORTANT — Cross-user token uniqueness:
|
|
34
|
+
/// The backend `POST /users/{userId}/devices` endpoint MUST guarantee that
|
|
35
|
+
/// the FCM token is unique across users. When the same token is registered
|
|
36
|
+
/// for a new user, delete any existing record holding that same token under
|
|
37
|
+
/// other users. Without this, a failed logout (offline) leaves the phone
|
|
38
|
+
/// registered to both accounts, and push for account A delivers to a phone
|
|
39
|
+
/// now signed in as account B.
|
|
32
40
|
Future<DeviceEntity> register(String userId, DeviceEntity device);
|
|
33
41
|
|
|
34
42
|
/// Update the device in the backend
|
|
@@ -38,6 +46,25 @@ abstract class DeviceApi {
|
|
|
38
46
|
/// Unregister the device in the backend
|
|
39
47
|
Future<void> unregister(String userId, String deviceId);
|
|
40
48
|
|
|
49
|
+
/// Heartbeat — tell the backend this install is still active.
|
|
50
|
+
/// Backend should update a `lastUpdateDate` (or equivalent) timestamp and use
|
|
51
|
+
/// it to skip stale devices when sending push notifications. Without this,
|
|
52
|
+
/// re-installing the app (Xcode -> TestFlight, build updates) leaves
|
|
53
|
+
/// orphan device records that still receive push, causing duplicates.
|
|
54
|
+
///
|
|
55
|
+
/// Suggested endpoint: `PATCH /users/{userId}/devices/{installationId}/touch`
|
|
56
|
+
/// Implement on your API to update the device row's last-seen timestamp.
|
|
57
|
+
Future<void> touch(String userId, String installationId);
|
|
58
|
+
|
|
59
|
+
/// Ask the backend to drop device records of the same user that haven't
|
|
60
|
+
/// been heartbeated in a while (typically 30 days). Called after registering
|
|
61
|
+
/// a fresh installation to clean up orphans from previous installs on the
|
|
62
|
+
/// same physical device.
|
|
63
|
+
///
|
|
64
|
+
/// Suggested endpoint: `POST /users/{userId}/devices/cleanup-stale`
|
|
65
|
+
/// with body `{ "currentInstallationId": "...", "olderThanDays": 30 }`.
|
|
66
|
+
Future<void> cleanupStaleDevices(String userId, String currentInstallationId);
|
|
67
|
+
|
|
41
68
|
/// Listen to token refresh
|
|
42
69
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh);
|
|
43
70
|
|
|
@@ -153,6 +180,32 @@ class FirebaseDeviceApi implements DeviceApi {
|
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
183
|
+
@override
|
|
184
|
+
Future<void> touch(String userId, String installationId) async {
|
|
185
|
+
// Fire-and-forget: silently no-ops if the backend doesn't implement
|
|
186
|
+
// the touch endpoint yet. The duplicated-push protection becomes active
|
|
187
|
+
// once the API exposes the endpoint described in the abstract above.
|
|
188
|
+
try {
|
|
189
|
+
await _client.patch('/users/$userId/devices/$installationId/touch');
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
Future<void> cleanupStaleDevices(
|
|
195
|
+
String userId,
|
|
196
|
+
String currentInstallationId,
|
|
197
|
+
) async {
|
|
198
|
+
try {
|
|
199
|
+
await _client.post(
|
|
200
|
+
'/users/$userId/devices/cleanup-stale',
|
|
201
|
+
data: {
|
|
202
|
+
'currentInstallationId': currentInstallationId,
|
|
203
|
+
'olderThanDays': 30,
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
} catch (_) {}
|
|
207
|
+
}
|
|
208
|
+
|
|
156
209
|
@override
|
|
157
210
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh) {
|
|
158
211
|
_onTokenRefreshSubscription =
|
|
@@ -108,7 +108,7 @@ void run(SharedPreferences prefs) => runApp(
|
|
|
108
108
|
// mode: ThemeMode.dark,
|
|
109
109
|
// ),
|
|
110
110
|
// See ./docs/theme.md for more details
|
|
111
|
-
class MyApp extends
|
|
111
|
+
class MyApp extends ConsumerStatefulWidget {
|
|
112
112
|
final SharedPreferences sharedPreferences;
|
|
113
113
|
|
|
114
114
|
const MyApp({
|
|
@@ -116,23 +116,42 @@ class MyApp extends ConsumerWidget {
|
|
|
116
116
|
required this.sharedPreferences,
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
@override
|
|
120
|
+
ConsumerState<MyApp> createState() => _MyAppState();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class _MyAppState extends ConsumerState<MyApp> {
|
|
124
|
+
late final AppTheme _appTheme;
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
void initState() {
|
|
128
|
+
super.initState();
|
|
129
|
+
_appTheme = AppTheme.uniform(
|
|
130
|
+
sharedPreferences: widget.sharedPreferences,
|
|
131
|
+
themeFactory: const UniversalThemeFactory(),
|
|
132
|
+
lightColors: KasyColors.light(),
|
|
133
|
+
darkColors: KasyColors.dark(),
|
|
134
|
+
textTheme: KasyTextTheme.build(),
|
|
135
|
+
defaultMode: ThemeMode.system,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
void dispose() {
|
|
141
|
+
_appTheme.dispose();
|
|
142
|
+
super.dispose();
|
|
143
|
+
}
|
|
144
|
+
|
|
119
145
|
// This widget is the root of your application.
|
|
120
146
|
@override
|
|
121
|
-
Widget build(BuildContext context
|
|
147
|
+
Widget build(BuildContext context) {
|
|
122
148
|
ErrorWidget.builder = (FlutterErrorDetails details) {
|
|
123
149
|
return AppErrorWidget(error: details);
|
|
124
150
|
};
|
|
125
151
|
final goRouter = ref.watch(goRouterProvider);
|
|
126
152
|
|
|
127
153
|
return ThemeProvider(
|
|
128
|
-
notifier:
|
|
129
|
-
sharedPreferences: sharedPreferences,
|
|
130
|
-
themeFactory: const UniversalThemeFactory(),
|
|
131
|
-
lightColors: KasyColors.light(),
|
|
132
|
-
darkColors: KasyColors.dark(),
|
|
133
|
-
textTheme: KasyTextTheme.build(),
|
|
134
|
-
defaultMode: ThemeMode.light,
|
|
135
|
-
),
|
|
154
|
+
notifier: _appTheme,
|
|
136
155
|
child: Builder(builder: (context) {
|
|
137
156
|
return WebDevicePreview.wrap(
|
|
138
157
|
child: DevInspector.wrap(
|
|
@@ -98,11 +98,21 @@ flutter_launcher_icons:
|
|
|
98
98
|
android: ic_launcher
|
|
99
99
|
ios: true
|
|
100
100
|
remove_alpha_ios: true
|
|
101
|
+
web:
|
|
102
|
+
generate: true
|
|
103
|
+
image_path: assets/images/favicon.png
|
|
104
|
+
background_color: "#01171f"
|
|
105
|
+
theme_color: "#01171f"
|
|
101
106
|
flutter_native_splash:
|
|
102
107
|
color: "#FFFFFF"
|
|
108
|
+
color_dark: "#000000"
|
|
103
109
|
fullscreen: true
|
|
104
110
|
ios: true
|
|
105
111
|
android: true
|
|
106
|
-
image: assets/images/
|
|
112
|
+
image: assets/images/splash_logo_light.png
|
|
113
|
+
image_dark: assets/images/splash_logo_dark.png
|
|
107
114
|
android_12:
|
|
108
115
|
color: "#FFFFFF"
|
|
116
|
+
color_dark: "#000000"
|
|
117
|
+
image: assets/images/splash_logo_light.png
|
|
118
|
+
image_dark: assets/images/splash_logo_dark.png
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* Original hardcoded values in Firebase/:
|
|
11
11
|
* - package name : kasy_kit (Dart import path prefix)
|
|
12
12
|
* - bundle ID : com.aicrus.firebase.kit (Android namespace, iOS bundle ID)
|
|
13
|
-
* - app display :
|
|
13
|
+
* - app display : Kasy App (AndroidManifest, Info.plist) — unique string with space, won't collide with KasyButton/KasyTheme/etc
|
|
14
14
|
* - short name : appfirebase (kAppName Dart constant)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const ORIGINAL_PACKAGE = 'kasy_kit';
|
|
18
18
|
const ORIGINAL_BUNDLE_ID = 'com.aicrus.firebase.kit';
|
|
19
|
-
const ORIGINAL_APP_NAME = '
|
|
19
|
+
const ORIGINAL_APP_NAME = 'Kasy App';
|
|
20
20
|
const ORIGINAL_SHORT_NAME = 'appfirebase';
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -254,11 +254,17 @@ Deno.serve(async (req: Request) => {
|
|
|
254
254
|
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
255
255
|
const supabase = createClient(supabaseUrl, serviceRoleKey);
|
|
256
256
|
|
|
257
|
-
// Fetch device tokens for the user
|
|
257
|
+
// Fetch device tokens for the user, skipping orphan installs.
|
|
258
|
+
// Devices not touched in the last 60 days are treated as leftovers from
|
|
259
|
+
// previous installations on the same physical device (each install gets a
|
|
260
|
+
// fresh installation_id). Sending to them causes duplicated push delivery.
|
|
261
|
+
const STALE_DEVICE_TTL_MS = 60 * 24 * 60 * 60 * 1000;
|
|
262
|
+
const cutoffIso = new Date(Date.now() - STALE_DEVICE_TTL_MS).toISOString();
|
|
258
263
|
const { data: devices, error: devErr } = await supabase
|
|
259
264
|
.from("devices")
|
|
260
265
|
.select("id, token")
|
|
261
|
-
.eq("user_id", notification.user_id)
|
|
266
|
+
.eq("user_id", notification.user_id)
|
|
267
|
+
.or(`last_update_date.is.null,last_update_date.gte.${cutoffIso}`);
|
|
262
268
|
|
|
263
269
|
if (devErr || !devices?.length) {
|
|
264
270
|
console.log(`[send-push] no devices for user ${notification.user_id}`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Cross-user device token deduplication.
|
|
2
|
+
--
|
|
3
|
+
-- Guarantees that a single FCM token belongs to at most one user at any time.
|
|
4
|
+
-- When the same install registers under a new account (typical scenario:
|
|
5
|
+
-- logout failed offline), the old row is automatically deleted so the iPhone
|
|
6
|
+
-- only receives push for the account currently signed in.
|
|
7
|
+
--
|
|
8
|
+
-- Winner = the row that was just inserted/updated (most recent intent).
|
|
9
|
+
|
|
10
|
+
CREATE OR REPLACE FUNCTION public.cleanup_duplicate_device_tokens()
|
|
11
|
+
RETURNS TRIGGER
|
|
12
|
+
LANGUAGE plpgsql
|
|
13
|
+
SECURITY DEFINER
|
|
14
|
+
SET search_path = public
|
|
15
|
+
AS $$
|
|
16
|
+
BEGIN
|
|
17
|
+
IF NEW.token IS NULL OR NEW.token = '' THEN
|
|
18
|
+
RETURN NEW;
|
|
19
|
+
END IF;
|
|
20
|
+
|
|
21
|
+
DELETE FROM public.devices
|
|
22
|
+
WHERE token = NEW.token
|
|
23
|
+
AND id <> NEW.id;
|
|
24
|
+
|
|
25
|
+
RETURN NEW;
|
|
26
|
+
END;
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
DROP TRIGGER IF EXISTS trg_cleanup_duplicate_device_tokens ON public.devices;
|
|
30
|
+
|
|
31
|
+
CREATE TRIGGER trg_cleanup_duplicate_device_tokens
|
|
32
|
+
AFTER INSERT OR UPDATE OF token ON public.devices
|
|
33
|
+
FOR EACH ROW
|
|
34
|
+
EXECUTE FUNCTION public.cleanup_duplicate_device_tokens();
|
|
@@ -36,6 +36,15 @@ abstract class DeviceApi {
|
|
|
36
36
|
/// Unregister the device in the backend
|
|
37
37
|
Future<void> unregister(String userId, String deviceId);
|
|
38
38
|
|
|
39
|
+
/// Heartbeat — update `last_update_date` on the current device row.
|
|
40
|
+
/// Used so the backend can detect orphan rows from previous installs.
|
|
41
|
+
Future<void> touch(String userId, String installationId);
|
|
42
|
+
|
|
43
|
+
/// Delete device rows of the same user that haven't been touched in a while.
|
|
44
|
+
/// Called after registering a fresh installation to remove orphans left by
|
|
45
|
+
/// previous installs (whose installation_id no longer matches).
|
|
46
|
+
Future<void> cleanupStaleDevices(String userId, String currentInstallationId);
|
|
47
|
+
|
|
39
48
|
/// Listen to token refresh
|
|
40
49
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh);
|
|
41
50
|
|
|
@@ -160,6 +169,40 @@ class FirebaseDeviceApi implements DeviceApi {
|
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
@override
|
|
173
|
+
Future<void> touch(String userId, String installationId) async {
|
|
174
|
+
try {
|
|
175
|
+
await _client
|
|
176
|
+
.from('devices')
|
|
177
|
+
.update({'last_update_date': DateTime.now().toIso8601String()})
|
|
178
|
+
.eq('user_id', userId)
|
|
179
|
+
.eq('installation_id', installationId);
|
|
180
|
+
} catch (_) {
|
|
181
|
+
// Missing row — caller will re-register on next session.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@override
|
|
186
|
+
Future<void> cleanupStaleDevices(
|
|
187
|
+
String userId,
|
|
188
|
+
String currentInstallationId,
|
|
189
|
+
) async {
|
|
190
|
+
// Devices not touched in the last 30 days are treated as orphans from
|
|
191
|
+
// previous installations on the same physical device. Active second
|
|
192
|
+
// devices stay above this threshold via heartbeat.
|
|
193
|
+
final cutoff = DateTime.now().subtract(const Duration(days: 30));
|
|
194
|
+
try {
|
|
195
|
+
await _client
|
|
196
|
+
.from('devices')
|
|
197
|
+
.delete()
|
|
198
|
+
.eq('user_id', userId)
|
|
199
|
+
.neq('installation_id', currentInstallationId)
|
|
200
|
+
.lt('last_update_date', cutoff.toIso8601String());
|
|
201
|
+
} catch (e) {
|
|
202
|
+
Logger().w('cleanupStaleDevices failed: $e');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
163
206
|
@override
|
|
164
207
|
Future<void> clear(String userId) async {
|
|
165
208
|
try {
|
|
@@ -138,7 +138,7 @@ void run(SharedPreferences prefs) => runApp(
|
|
|
138
138
|
// mode: ThemeMode.dark,
|
|
139
139
|
// ),
|
|
140
140
|
// See ./docs/theme.md for more details
|
|
141
|
-
class MyApp extends
|
|
141
|
+
class MyApp extends ConsumerStatefulWidget {
|
|
142
142
|
final SharedPreferences sharedPreferences;
|
|
143
143
|
|
|
144
144
|
const MyApp({
|
|
@@ -146,23 +146,42 @@ class MyApp extends ConsumerWidget {
|
|
|
146
146
|
required this.sharedPreferences,
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
@override
|
|
150
|
+
ConsumerState<MyApp> createState() => _MyAppState();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class _MyAppState extends ConsumerState<MyApp> {
|
|
154
|
+
late final AppTheme _appTheme;
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
void initState() {
|
|
158
|
+
super.initState();
|
|
159
|
+
_appTheme = AppTheme.uniform(
|
|
160
|
+
sharedPreferences: widget.sharedPreferences,
|
|
161
|
+
themeFactory: const UniversalThemeFactory(),
|
|
162
|
+
lightColors: KasyColors.light(),
|
|
163
|
+
darkColors: KasyColors.dark(),
|
|
164
|
+
textTheme: KasyTextTheme.build(),
|
|
165
|
+
defaultMode: ThemeMode.system,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@override
|
|
170
|
+
void dispose() {
|
|
171
|
+
_appTheme.dispose();
|
|
172
|
+
super.dispose();
|
|
173
|
+
}
|
|
174
|
+
|
|
149
175
|
// This widget is the root of your application.
|
|
150
176
|
@override
|
|
151
|
-
Widget build(BuildContext context
|
|
177
|
+
Widget build(BuildContext context) {
|
|
152
178
|
ErrorWidget.builder = (FlutterErrorDetails details) {
|
|
153
179
|
return AppErrorWidget(error: details);
|
|
154
180
|
};
|
|
155
181
|
final goRouter = ref.watch(goRouterProvider);
|
|
156
182
|
|
|
157
183
|
return ThemeProvider(
|
|
158
|
-
notifier:
|
|
159
|
-
sharedPreferences: sharedPreferences,
|
|
160
|
-
themeFactory: const UniversalThemeFactory(),
|
|
161
|
-
lightColors: KasyColors.light(),
|
|
162
|
-
darkColors: KasyColors.dark(),
|
|
163
|
-
textTheme: KasyTextTheme.build(),
|
|
164
|
-
defaultMode: ThemeMode.light,
|
|
165
|
-
),
|
|
184
|
+
notifier: _appTheme,
|
|
166
185
|
child: Builder(builder: (context) {
|
|
167
186
|
return WebDevicePreview.wrap(
|
|
168
187
|
child: DevInspector.wrap(
|
|
@@ -100,11 +100,21 @@ flutter_launcher_icons:
|
|
|
100
100
|
android: ic_launcher
|
|
101
101
|
ios: true
|
|
102
102
|
remove_alpha_ios: true
|
|
103
|
+
web:
|
|
104
|
+
generate: true
|
|
105
|
+
image_path: assets/images/favicon.png
|
|
106
|
+
background_color: "#01171f"
|
|
107
|
+
theme_color: "#01171f"
|
|
103
108
|
flutter_native_splash:
|
|
104
109
|
color: "#FFFFFF"
|
|
110
|
+
color_dark: "#000000"
|
|
105
111
|
fullscreen: true
|
|
106
112
|
ios: true
|
|
107
113
|
android: true
|
|
108
|
-
image: assets/images/
|
|
114
|
+
image: assets/images/splash_logo_light.png
|
|
115
|
+
image_dark: assets/images/splash_logo_dark.png
|
|
109
116
|
android_12:
|
|
110
117
|
color: "#FFFFFF"
|
|
118
|
+
color_dark: "#000000"
|
|
119
|
+
image: assets/images/splash_logo_light.png
|
|
120
|
+
image_dark: assets/images/splash_logo_dark.png
|
|
@@ -1,151 +1,27 @@
|
|
|
1
1
|
# Feature Patches
|
|
2
2
|
|
|
3
|
-
Este diretório
|
|
3
|
+
Este diretório guarda **patches por feature**: arquivos extras que são copiados para o projeto gerado quando o usuário seleciona um módulo específico no wizard do CLI.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Estado atual
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
features/
|
|
9
|
-
ci/ ← patch aplicado quando o usuário seleciona o módulo "ci"
|
|
10
|
-
.github/
|
|
11
|
-
workflows/
|
|
12
|
-
app.yml
|
|
13
|
-
.gitlab-ci.yml
|
|
14
|
-
...
|
|
15
|
-
web/ ← patch aplicado quando "web" é selecionado
|
|
16
|
-
web/
|
|
17
|
-
index.html
|
|
18
|
-
...
|
|
19
|
-
widget/ ← patch aplicado quando "widget" é selecionado
|
|
20
|
-
android/
|
|
21
|
-
...
|
|
22
|
-
```
|
|
7
|
+
Este diretório está intencionalmente vazio (só este README). Todas as features já vivem dentro de `Firebase/` e são incluídas em `cli/templates/firebase/` pelo `cli/scripts/bundle-template.js` no momento do publish. Features opcionais são controladas por `removeModuleDirs()` em `cli/lib/scaffold/shared/generator-utils.js` — quando a feature não é selecionada, sua pasta é removida do projeto gerado.
|
|
23
8
|
|
|
24
|
-
|
|
9
|
+
## Quando criar um patch aqui
|
|
25
10
|
|
|
26
|
-
|
|
11
|
+
Apenas quando a feature precisar de arquivos que **não existem** em `Firebase/`. Se o mesmo caminho já existe em `Firebase/`, **não crie patch** — o patch vai sobrescrever silenciosamente o template atualizado pela versão (provavelmente antiga) que estiver no patch, causando regressão no projeto do cliente (perda de integrações, validações, UI nova, etc.).
|
|
27
12
|
|
|
28
|
-
##
|
|
13
|
+
## Como adicionar um patch
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
| `widget` | Não | Os arquivos do widget (iOS + Android + Dart) já vão no template base. Quando o módulo não é selecionado, `removeAndroidWidgetArtifacts` + `writeNoOpAdminHomeWidgets` em `generate.js` limpam o que sobra. |
|
|
35
|
-
| `llm_chat` | Não | Apenas `LLM_CHAT_ENDPOINT` via dart-define. A chave da API LLM fica no servidor (Firebase Secret / Supabase Secret) — nunca no app. |
|
|
36
|
-
| `sentry` | Não | Apenas dart-define (`SENTRY_DSN`) |
|
|
37
|
-
| `analytics` | Não | Apenas dart-define |
|
|
38
|
-
| `facebook` | Não | Token já substituído via `buildTokens` |
|
|
39
|
-
| `revenuecat`| Não | Apenas dart-define (`REVENUECAT_KEY_*`) |
|
|
40
|
-
| `onboarding`| Não | Habilitado via dart-define / `features_config.json` |
|
|
41
|
-
| `feedback` | Não | Habilitado via dart-define / `features_config.json` |
|
|
42
|
-
| `local_notifications` | ✅ Sim | Copia `lib/features/local_reminder/` com UI, provider e repositório |
|
|
15
|
+
1. Crie `features/{modulo}/`.
|
|
16
|
+
2. Coloque os arquivos exatamente como devem aparecer no projeto gerado (caminhos relativos à raiz do projeto).
|
|
17
|
+
3. Adicione o nome do módulo a `ALLOWED_FEATURE_PATCHES` em `cli/scripts/check-feature-patches.js`, com um comentário de uma linha explicando por que o patch é necessário.
|
|
18
|
+
4. O engine aplica os patches depois de copiar o template base e o patch do backend.
|
|
43
19
|
|
|
44
|
-
##
|
|
20
|
+
## Drift guard
|
|
45
21
|
|
|
46
|
-
|
|
47
|
-
2. Coloque dentro dela os arquivos **exatamente como devem aparecer** no projeto gerado (caminhos relativos à raiz do projeto).
|
|
48
|
-
3. O engine aplica os patches após copiar o template base e após aplicar o patch do backend.
|
|
22
|
+
`cli/scripts/check-feature-patches.js` roda no `npm prepack` (antes de qualquer `npm publish` ou `npm pack`). O build falha se:
|
|
49
23
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
> precisam ser atualizados para chamar essa etapa quando a unificação for concluída.
|
|
24
|
+
- alguma pasta dentro de `features/` não estiver listada em `ALLOWED_FEATURE_PATCHES`, ou
|
|
25
|
+
- algum arquivo dentro de um patch permitido tiver caminho equivalente em `Firebase/` (significa que o patch é duplicata desatualizada).
|
|
53
26
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## Feature: `local_notifications` — Notificações Locais Agendadas
|
|
57
|
-
|
|
58
|
-
> **Plataformas:** iOS e Android apenas (sem suporte Web/Desktop).
|
|
59
|
-
|
|
60
|
-
### O que esta feature faz
|
|
61
|
-
|
|
62
|
-
Permite ao usuário configurar um lembrete recorrente diretamente no app, sem servidor. A notificação é agendada localmente via `flutter_local_notifications` e persiste entre sessões via `SharedPreferences`.
|
|
63
|
-
|
|
64
|
-
### Tipos de lembrete suportados
|
|
65
|
-
|
|
66
|
-
| Tipo | Comportamento |
|
|
67
|
-
|-----------------|------------------------------------------------------|
|
|
68
|
-
| `daily` | Dispara todo dia no horário escolhido |
|
|
69
|
-
| `weekly` | Dispara uma vez por semana no dia + horário escolhido |
|
|
70
|
-
| `specificDate` | Dispara uma única vez na data + hora escolhida |
|
|
71
|
-
|
|
72
|
-
### Entrada do usuário
|
|
73
|
-
|
|
74
|
-
Acesso via **Settings → Lembretes** (`/reminder`):
|
|
75
|
-
|
|
76
|
-
1. **Toggle "Ativar lembrete"** — liga/desliga (cancela notificação pendente ao desligar)
|
|
77
|
-
2. **Segmented button "Repetir"** — escolhe o tipo: `Todo dia / Toda semana / Data específica`
|
|
78
|
-
3. **Horário** — `TimePicker` nativo do sistema
|
|
79
|
-
4. **Dia da semana** — `ChoiceChip` (seg–dom), aparece somente no modo semanal
|
|
80
|
-
5. **Data e hora** — `DatePicker` + `TimePicker` em sequência, aparece somente no modo data específica
|
|
81
|
-
|
|
82
|
-
### Estrutura de arquivos gerados no projeto
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
lib/features/local_reminder/
|
|
86
|
-
repositories/
|
|
87
|
-
reminder_preferences.dart ← SharedPreferences (load/save ReminderState)
|
|
88
|
-
providers/
|
|
89
|
-
reminder_notifier.dart ← AsyncNotifier (keepAlive) + agenda/cancela
|
|
90
|
-
reminder_notifier.g.dart ← gerado pelo build_runner (provider: reminderProvider)
|
|
91
|
-
ui/
|
|
92
|
-
reminder_page.dart ← ReminderPage + _ReminderForm + subwidgets
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Integrações automáticas pelo CLI
|
|
96
|
-
|
|
97
|
-
| Arquivo | O que é adicionado |
|
|
98
|
-
|--------------------------------|---------------------------------------------------------|
|
|
99
|
-
| `lib/core/config/features.dart` | `const bool withLocalNotifications = true/false;` |
|
|
100
|
-
| `lib/router.dart` | Import + `GoRoute(path: '/reminder', ...)` |
|
|
101
|
-
| `lib/features/settings/settings_page.dart` | Tile "Lembretes" → `context.push('/reminder')` |
|
|
102
|
-
|
|
103
|
-
### Model — `ReminderState`
|
|
104
|
-
|
|
105
|
-
```dart
|
|
106
|
-
class ReminderState {
|
|
107
|
-
final bool enabled;
|
|
108
|
-
final ReminderType type; // daily | weekly | specificDate
|
|
109
|
-
final int hour;
|
|
110
|
-
final int minute;
|
|
111
|
-
final int dayOfWeek; // 1=Segunda … 7=Domingo
|
|
112
|
-
final DateTime? date; // usado somente para specificDate
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### Notificação agendada
|
|
117
|
-
|
|
118
|
-
ID fixo `42`. Texto configurado via i18n (`dailyReminder.title` / `dailyReminder.body`).
|
|
119
|
-
Ao salvar qualquer mudança, o provider cancela o ID 42 e re-agenda com os novos parâmetros.
|
|
120
|
-
|
|
121
|
-
### Dependências (já no core — não adicionadas pelo módulo)
|
|
122
|
-
|
|
123
|
-
- `flutter_local_notifications`
|
|
124
|
-
- `flutter_timezone` + `timezone`
|
|
125
|
-
- `shared_preferences`
|
|
126
|
-
- `riverpod_annotation`
|
|
127
|
-
|
|
128
|
-
### Traduções adicionadas
|
|
129
|
-
|
|
130
|
-
Chaves em `lib/i18n/{pt,en,es}.i18n.json`:
|
|
131
|
-
|
|
132
|
-
```json
|
|
133
|
-
"reminderPage": {
|
|
134
|
-
"title": "Lembretes",
|
|
135
|
-
"toggleLabel": "Ativar lembrete",
|
|
136
|
-
"typeLabel": "Repetir",
|
|
137
|
-
"daily": "Todo dia",
|
|
138
|
-
"weekly": "Toda semana",
|
|
139
|
-
"specificDate": "Data específica",
|
|
140
|
-
"timeLabel": "Horário",
|
|
141
|
-
"dayLabel": "Dia da semana",
|
|
142
|
-
"dateLabel": "Data e hora",
|
|
143
|
-
"selectDate": "Selecionar data e hora"
|
|
144
|
-
},
|
|
145
|
-
"dailyReminder": { "title": "Lembrete", "body": "Está na hora de beber água." },
|
|
146
|
-
"settings": { "reminders": "Lembretes" }
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Bug corrigido durante a implementação
|
|
150
|
-
|
|
151
|
-
`local_notifier.dart` tinha um método `scheduleFromNow()` com lógica invertida (agendava apenas quando **não** havia notificações pendentes) e usava `Future.delayed` que não sobrevive ao restart do app. O método foi **removido** completamente.
|
|
27
|
+
Esse é o mecanismo que impede o cenário em que patches divergem de `Firebase/` ao longo do tempo e o CLI passa a entregar código velho para os clientes.
|
|
@@ -273,7 +273,6 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
|
|
|
273
273
|
lines.push(`import 'package:${pkg}/features/authentication/ui/signin_page.dart';`);
|
|
274
274
|
lines.push(`import 'package:${pkg}/features/authentication/ui/signup_page.dart';`);
|
|
275
275
|
if (withFeedback) {
|
|
276
|
-
lines.push(`import 'package:${pkg}/features/feedbacks/ui/component/add_feature_form.dart';`);
|
|
277
276
|
lines.push(`import 'package:${pkg}/features/feedbacks/ui/feedback_page.dart';`);
|
|
278
277
|
}
|
|
279
278
|
if (withLlmChat) {
|
|
@@ -374,11 +373,6 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
|
|
|
374
373
|
lines.push(` path: '/feedback',`);
|
|
375
374
|
lines.push(` builder: (context, state) => const FeedbackPage(),`);
|
|
376
375
|
lines.push(` ),`);
|
|
377
|
-
lines.push(` GoRoute(`);
|
|
378
|
-
lines.push(` name: 'feedback_new',`);
|
|
379
|
-
lines.push(` path: '/feedback/new',`);
|
|
380
|
-
lines.push(` builder: (context, state) => const AddFeatureComponent(),`);
|
|
381
|
-
lines.push(` ),`);
|
|
382
376
|
}
|
|
383
377
|
|
|
384
378
|
// LLM Chat
|
|
@@ -596,6 +590,13 @@ class NoOpAnalyticsApi implements AnalyticsApi {
|
|
|
596
590
|
@override Future<void> identify(User user) async {}
|
|
597
591
|
}
|
|
598
592
|
|
|
593
|
+
// Stub kept for source-compat with code that still calls MixpanelAnalyticsApi.instance().
|
|
594
|
+
// Run: kasy add analytics to swap this file for the real Mixpanel-backed implementation.
|
|
595
|
+
class MixpanelAnalyticsApi extends NoOpAnalyticsApi {
|
|
596
|
+
const MixpanelAnalyticsApi._() : super();
|
|
597
|
+
factory MixpanelAnalyticsApi.instance() => const MixpanelAnalyticsApi._();
|
|
598
|
+
}
|
|
599
|
+
|
|
599
600
|
class AnalyticsObserver extends RouteObserver<ModalRoute<dynamic>> {
|
|
600
601
|
final AnalyticsApi analyticsApi;
|
|
601
602
|
final String? prefix;
|
|
@@ -1186,11 +1187,8 @@ async function stripPubspecDeps(projectDir, modules) {
|
|
|
1186
1187
|
}
|
|
1187
1188
|
|
|
1188
1189
|
/**
|
|
1189
|
-
* Patches
|
|
1190
|
+
* Patches files that always import sentry_flutter, removing the dependency
|
|
1190
1191
|
* when neither sentry, revenuecat, nor facebook modules are selected.
|
|
1191
|
-
* Affected files:
|
|
1192
|
-
* - lib/core/initializer/onstart_widget.dart
|
|
1193
|
-
* - lib/core/data/api/remote_config_api.dart
|
|
1194
1192
|
*
|
|
1195
1193
|
* @param {string} projectDir
|
|
1196
1194
|
*/
|
|
@@ -1200,15 +1198,16 @@ async function writeNoOpSentryUsages(projectDir) {
|
|
|
1200
1198
|
const files = [
|
|
1201
1199
|
path.join(projectDir, 'lib', 'core', 'initializer', 'onstart_widget.dart'),
|
|
1202
1200
|
path.join(projectDir, 'lib', 'core', 'data', 'api', 'remote_config_api.dart'),
|
|
1201
|
+
path.join(projectDir, 'lib', 'features', 'notifications', 'api', 'local_notifier.dart'),
|
|
1202
|
+
path.join(projectDir, 'lib', 'features', 'notifications', 'providers', 'models', 'notification.dart'),
|
|
1203
|
+
path.join(projectDir, 'lib', 'features', 'notifications', 'shared', 'notification_permission_bottom_sheet.dart'),
|
|
1203
1204
|
];
|
|
1204
1205
|
|
|
1205
1206
|
for (const filePath of files) {
|
|
1206
1207
|
if (!(await fs.pathExists(filePath))) continue;
|
|
1207
1208
|
let content = await fs.readFile(filePath, 'utf8');
|
|
1208
|
-
// Remove sentry import line
|
|
1209
1209
|
content = content.replace(sentryImport, '');
|
|
1210
|
-
|
|
1211
|
-
content = content.replace(/^[ \t]*Sentry\.captureException\([^)]+\);\n/gm, '');
|
|
1210
|
+
content = content.replace(/^[ \t]*Sentry\.captureException\([^)]*\);\n/gm, '');
|
|
1212
1211
|
await fs.outputFile(filePath, content, 'utf8');
|
|
1213
1212
|
}
|
|
1214
1213
|
}
|
|
@@ -1469,11 +1468,13 @@ class PremiumPageArgs {
|
|
|
1469
1468
|
'utf8',
|
|
1470
1469
|
);
|
|
1471
1470
|
|
|
1472
|
-
// 4. No-op premium_page_factory.dart (used by admin_paywalls)
|
|
1471
|
+
// 4. No-op premium_page_factory.dart (used by admin_paywalls).
|
|
1472
|
+
// Mirror the constants declared in Firebase/.../premium_page_factory.dart so that
|
|
1473
|
+
// admin_routes.dart compiles even without revenuecat selected.
|
|
1473
1474
|
await fs.outputFile(
|
|
1474
1475
|
path.join(projectDir, 'lib', 'features', 'subscription', 'ui', 'component', 'premium_page_factory.dart'),
|
|
1475
1476
|
`// No-op paywall factory. Run: kasy add revenuecat to activate.
|
|
1476
|
-
enum PaywallFactory { basic }
|
|
1477
|
+
enum PaywallFactory { basic, basicRow, minimal, withSwitch }
|
|
1477
1478
|
`,
|
|
1478
1479
|
'utf8',
|
|
1479
1480
|
);
|