kasy-cli 1.19.3 → 1.20.1

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.
Files changed (83) hide show
  1. package/README.md +11 -3
  2. package/bin/kasy.js +1 -0
  3. package/lib/commands/new.js +87 -37
  4. package/lib/commands/run.js +14 -0
  5. package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
  6. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  7. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
  9. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
  10. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
  11. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
  12. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
  14. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  17. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
  18. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
  19. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
  20. package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
  21. package/lib/scaffold/backends/supabase/deploy.js +56 -3
  22. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
  23. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
  24. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
  25. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  26. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  27. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
  28. package/lib/scaffold/catalog.js +2 -2
  29. package/lib/scaffold/engine.js +5 -0
  30. package/lib/scaffold/generate.js +23 -3
  31. package/lib/scaffold/shared/generator-utils.js +303 -56
  32. package/lib/scaffold/shared/post-build.js +11 -0
  33. package/lib/utils/i18n/messages-en.js +6 -1
  34. package/lib/utils/i18n/messages-es.js +6 -1
  35. package/lib/utils/i18n/messages-pt.js +6 -1
  36. package/package.json +1 -1
  37. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  42. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  43. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  44. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  45. package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
  46. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1150 -0
  47. package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
  48. package/templates/firebase/lib/components/kasy_text_field.dart +37 -34
  49. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +13 -82
  50. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -102
  51. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
  52. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +433 -243
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
  54. package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
  55. package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
  56. package/templates/firebase/lib/core/theme/colors.dart +6 -2
  57. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
  58. package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
  59. package/templates/firebase/lib/features/home/home_components_page.dart +11 -14
  60. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
  61. package/templates/firebase/lib/features/home/home_page.dart +7 -8
  62. package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
  63. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
  64. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
  65. package/templates/firebase/lib/i18n/en.i18n.json +3 -1
  66. package/templates/firebase/lib/i18n/es.i18n.json +3 -1
  67. package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
  68. package/templates/firebase/lib/router.dart +60 -0
  69. package/templates/firebase/pubspec.yaml +6 -4
  70. package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
  71. package/templates/firebase/web/index.html +7 -17
  72. package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  73. package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
  74. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  75. package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
  76. package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
  77. package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  78. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  79. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
  80. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  81. package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
  82. package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
  83. package/templates/firebase/lib/firebase_options.dart +0 -75
@@ -229,6 +229,39 @@ async function writeFirebaserc(projectDir, firebaseProjectId) {
229
229
  );
230
230
  }
231
231
 
232
+ /**
233
+ * Strip the kit's own Firebase project from the generated firebase.json.
234
+ *
235
+ * flutterfire configure rewrites the android/ios/web platform entries for the
236
+ * user's project, but it keys the dart entry by its --out path
237
+ * (lib/firebase_options_dev.dart) and leaves the template's stale
238
+ * `flutter.platforms.dart["lib/firebase_options.dart"]` entry untouched — which
239
+ * still points at the kit project (fir-kit-8e56b). We no longer ship that file
240
+ * (see ALWAYS_EXCLUDE_BASENAMES), so this drops the dead entry to guarantee the
241
+ * user's firebase.json never references the kit project.
242
+ *
243
+ * Best effort: no-ops when firebase.json is missing, unparseable, or already clean.
244
+ *
245
+ * @param {string} projectDir
246
+ * @returns {Promise<{ ok: boolean, changed: boolean }>}
247
+ */
248
+ async function cleanFirebaseJsonKitRefs(projectDir) {
249
+ const firebaseJsonPath = path.join(projectDir, 'firebase.json');
250
+ if (!(await fs.pathExists(firebaseJsonPath))) return { ok: true, changed: false };
251
+ let config;
252
+ try {
253
+ config = await fs.readJson(firebaseJsonPath);
254
+ } catch {
255
+ return { ok: false, changed: false };
256
+ }
257
+ const dartMap = config && config.flutter && config.flutter.platforms && config.flutter.platforms.dart;
258
+ if (!dartMap || typeof dartMap !== 'object') return { ok: true, changed: false };
259
+ if (!('lib/firebase_options.dart' in dartMap)) return { ok: true, changed: false };
260
+ delete dartMap['lib/firebase_options.dart'];
261
+ await fs.writeJson(firebaseJsonPath, config, { spaces: 2 });
262
+ return { ok: true, changed: true };
263
+ }
264
+
232
265
  /**
233
266
  * Write environnements.dart overrides for backend-specific config.
234
267
  *
@@ -285,12 +318,14 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
285
318
  const withOnboarding = modules.includes('onboarding');
286
319
  const withRevenuecat = modules.includes('revenuecat');
287
320
  const withAnalytics = modules.includes('analytics');
321
+ const withWidget = modules.includes('widget');
288
322
 
289
323
  const fallback = withOnboarding ? '/onboarding' : '/signin';
290
324
 
291
325
  const lines = [];
292
326
 
293
327
  // ── Imports ────────────────────────────────────────────────────────────────
328
+ lines.push(`import 'package:flutter/foundation.dart';`);
294
329
  lines.push(`import 'package:flutter/material.dart';`);
295
330
  lines.push(`import 'package:flutter_riverpod/flutter_riverpod.dart';`);
296
331
  lines.push(`import 'package:go_router/go_router.dart';`);
@@ -307,6 +342,12 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
307
342
  if (withFeedback) {
308
343
  lines.push(`import 'package:${pkg}/features/feedbacks/ui/feedback_page.dart';`);
309
344
  }
345
+ // Home showcase detail screens — always present (not module-gated).
346
+ lines.push(`import 'package:${pkg}/features/home/design_system_page.dart';`);
347
+ lines.push(`import 'package:${pkg}/features/home/home_components_page.dart';`);
348
+ lines.push(`import 'package:${pkg}/features/home/home_components_preview_page.dart';`);
349
+ lines.push(`import 'package:${pkg}/features/home/home_components_preview_registry.dart';`);
350
+ lines.push(`import 'package:${pkg}/features/home/home_features_page.dart';`);
310
351
  if (withLlmChat) {
311
352
  lines.push(`import 'package:${pkg}/features/llm_chat/llm_chat_page.dart';`);
312
353
  }
@@ -316,10 +357,22 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
316
357
  if (withOnboarding) {
317
358
  lines.push(`import 'package:${pkg}/features/onboarding/ui/onboarding_page.dart';`);
318
359
  }
360
+ // Admin tools: send-push is always available; paywalls/home-widgets only
361
+ // when their module exists. The screens themselves are gated by kDebugMode.
362
+ // Kept in alphabetical order to satisfy directives_ordering.
363
+ if (withWidget) {
364
+ lines.push(`import 'package:${pkg}/features/settings/ui/components/admin/admin_home_widgets.dart';`);
365
+ }
366
+ if (withRevenuecat) {
367
+ lines.push(`import 'package:${pkg}/features/settings/ui/components/admin/admin_paywalls.dart';`);
368
+ }
369
+ lines.push(`import 'package:${pkg}/features/settings/ui/components/admin/admin_routes.dart';`);
370
+ lines.push(`import 'package:${pkg}/features/settings/ui/components/admin/send_push_notification_page.dart';`);
319
371
  if (withRevenuecat) {
320
372
  lines.push(`import 'package:${pkg}/features/subscription/ui/component/premium_page_factory.dart';`);
321
373
  lines.push(`import 'package:${pkg}/features/subscription/ui/premium_page.dart';`);
322
374
  }
375
+ lines.push(`import 'package:logger/logger.dart';`);
323
376
 
324
377
  // ── Provider ──────────────────────────────────────────────────────────────
325
378
  lines.push('');
@@ -341,7 +394,16 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
341
394
  lines.push(` return GoRouter(`);
342
395
  lines.push(` initialLocation: '/',`);
343
396
  lines.push(` navigatorKey: navigatorKey,`);
344
- lines.push(` errorBuilder: (context, state) => const PageNotFound(),`);
397
+ // Unknown routes (e.g. a stale deep link) redirect home instead of dead-ending
398
+ // on a 404. The /404 GoRoute below still serves explicit navigation to it.
399
+ lines.push(` onException: (context, state, router) {`);
400
+ lines.push(` Logger().w(`);
401
+ lines.push(` 'GoRouter caught unknown route → "\${state.uri}" '`);
402
+ lines.push(` '(matched: "\${state.matchedLocation}", error: \${state.error}). '`);
403
+ lines.push(` 'Redirecting to "/".',`);
404
+ lines.push(` );`);
405
+ lines.push(` router.go('/');`);
406
+ lines.push(` },`);
345
407
  lines.push(` observers: [`);
346
408
  if (withAnalytics) {
347
409
  lines.push(` AnalyticsObserver(analyticsApi: MixpanelAnalyticsApi.instance()),`);
@@ -360,6 +422,42 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
360
422
  lines.push(` ),`);
361
423
  lines.push(` ),`);
362
424
 
425
+ // Home showcase detail screens. TOP-LEVEL routes (siblings of '/', not children
426
+ // of the BottomMenu shell), so go_router renders them on the root navigator:
427
+ // full-screen, above the bottom bar, URL-addressable. Returning pops back to
428
+ // the tab with its menu intact.
429
+ lines.push(` GoRoute(`);
430
+ lines.push(` name: 'features',`);
431
+ lines.push(` path: '/features',`);
432
+ lines.push(` builder: (context, state) => const HomeFeaturesPage(),`);
433
+ lines.push(` ),`);
434
+ lines.push(` GoRoute(`);
435
+ lines.push(` name: 'design_system',`);
436
+ lines.push(` path: '/design-system',`);
437
+ lines.push(` builder: (context, state) => const DesignSystemPage(),`);
438
+ lines.push(` ),`);
439
+ lines.push(` GoRoute(`);
440
+ lines.push(` name: 'components',`);
441
+ lines.push(` path: '/components',`);
442
+ lines.push(` builder: (context, state) => const HomeComponentsPage(),`);
443
+ lines.push(` routes: [`);
444
+ lines.push(` GoRoute(`);
445
+ lines.push(` name: 'component_preview',`);
446
+ lines.push(` path: ':name',`);
447
+ lines.push(` builder: (context, state) {`);
448
+ lines.push(` final definition = getComponentPreviewDefinition(`);
449
+ lines.push(` state.pathParameters['name'] ?? '',`);
450
+ lines.push(` );`);
451
+ lines.push(` if (definition == null) return const PageNotFound();`);
452
+ lines.push(` return HomeComponentsPreviewPage(`);
453
+ lines.push(` title: definition.title,`);
454
+ lines.push(` variants: definition.variants,`);
455
+ lines.push(` );`);
456
+ lines.push(` },`);
457
+ lines.push(` ),`);
458
+ lines.push(` ],`);
459
+ lines.push(` ),`);
460
+
363
461
  // Onboarding
364
462
  if (withOnboarding) {
365
463
  lines.push(` GoRoute(`);
@@ -425,6 +523,64 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
425
523
  lines.push(` ),`);
426
524
  }
427
525
 
526
+ // Notifications + Settings: open the BottomMenu shell on the right tab.
527
+ // These also back the deep links from the settings/admin UI.
528
+ lines.push(` GoRoute(`);
529
+ lines.push(` name: 'notifications',`);
530
+ lines.push(` path: '/notifications',`);
531
+ lines.push(` builder: (context, state) => const UserInfosGuard(`);
532
+ lines.push(` fallbackRoute: '${fallback}',`);
533
+ lines.push(` child: BottomMenu(initialRoute: 'notifications'),`);
534
+ lines.push(` ),`);
535
+ lines.push(` ),`);
536
+ lines.push(` GoRoute(`);
537
+ lines.push(` name: 'settings',`);
538
+ lines.push(` path: '/settings',`);
539
+ lines.push(` builder: (context, state) => const UserInfosGuard(`);
540
+ lines.push(` fallbackRoute: '${fallback}',`);
541
+ lines.push(` child: BottomMenu(initialRoute: 'settings'),`);
542
+ lines.push(` ),`);
543
+ lines.push(` ),`);
544
+
545
+ // Send push notification (admin tool — always routable; the entry point in
546
+ // the settings admin sheet is itself debug-gated).
547
+ lines.push(` GoRoute(`);
548
+ lines.push(` name: 'send_push',`);
549
+ lines.push(` path: adminRouteSendPush,`);
550
+ lines.push(` builder: (context, state) => const SendPushNotificationPage(),`);
551
+ lines.push(` ),`);
552
+
553
+ // Debug-only admin screens (paywall preview / home widgets preview).
554
+ if (withRevenuecat || withWidget) {
555
+ lines.push(` if (kDebugMode) ...[`);
556
+ if (withRevenuecat) {
557
+ lines.push(` GoRoute(`);
558
+ lines.push(` name: 'admin_paywalls',`);
559
+ lines.push(` path: adminRoutePaywalls,`);
560
+ lines.push(` builder: (context, state) => const AdminPaywalls(),`);
561
+ lines.push(` ),`);
562
+ }
563
+ if (withWidget) {
564
+ lines.push(` GoRoute(`);
565
+ lines.push(` name: 'admin_home_widgets',`);
566
+ lines.push(` path: adminRouteHomeWidgets,`);
567
+ lines.push(` builder: (context, state) => const AdminHomeWidgets(),`);
568
+ lines.push(` ),`);
569
+ }
570
+ if (withRevenuecat) {
571
+ lines.push(` GoRoute(`);
572
+ lines.push(` name: 'admin_premium_preview',`);
573
+ lines.push(` path: '/admin/premium/:variant',`);
574
+ lines.push(` builder: (context, state) {`);
575
+ lines.push(` final paywall = paywallFactoryFromAdminRoute(state.pathParameters['variant']);`);
576
+ lines.push(` if (paywall == null) return const PageNotFound();`);
577
+ lines.push(` return PremiumPage(paywall: paywall);`);
578
+ lines.push(` },`);
579
+ lines.push(` ),`);
580
+ }
581
+ lines.push(` ],`);
582
+ }
583
+
428
584
  // Recover password + 404 (always)
429
585
  lines.push(` GoRoute(`);
430
586
  lines.push(` name: 'recover_password',`);
@@ -717,19 +873,28 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
717
873
  const withWidget = modules.includes('widget');
718
874
  const withRevenuecat = modules.includes('revenuecat');
719
875
  const withSupabase = backend === 'supabase';
720
- const withFirebase = backend === 'firebase' || backend === 'supabase'; // Firebase used for auth/notifications in all backends currently
876
+ // Firebase powers push notifications (FCM) and remote config in EVERY backend
877
+ // (Supabase additionally uses it for Google Sign-In). Every backend runs
878
+ // `flutterfire configure` during generation (see generate.js), so
879
+ // firebase_options_dev.dart and the google-services files always exist, which
880
+ // makes Firebase.initializeApp safe to call in all of them.
881
+ const withFirebase = true;
721
882
 
722
883
  const lines = [];
723
884
 
724
885
  // ── Imports ────────────────────────────────────────────────────────────────
886
+ lines.push(`import 'package:device_preview/device_preview.dart';`);
725
887
  if (withFirebase) {
726
888
  lines.push(`import 'package:firebase_core/firebase_core.dart';`);
889
+ lines.push(`import 'package:firebase_messaging/firebase_messaging.dart';`);
727
890
  }
728
891
  lines.push(`import 'package:flutter/foundation.dart';`);
729
892
  lines.push(`import 'package:flutter/material.dart';`);
730
893
  lines.push(`import 'package:flutter/services.dart';`);
731
894
  lines.push(`import 'package:flutter_localizations/flutter_localizations.dart';`);
895
+ lines.push(`import 'package:flutter_native_splash/flutter_native_splash.dart';`);
732
896
  lines.push(`import 'package:flutter_riverpod/flutter_riverpod.dart';`);
897
+ lines.push(`import 'package:jiffy/jiffy.dart';`);
733
898
  lines.push(`import 'package:logger/logger.dart';`);
734
899
  lines.push(`import 'package:${pkg}/core/config/app_env.dart';`);
735
900
  lines.push(`import 'package:${pkg}/core/data/api/analytics_api.dart';`);
@@ -739,9 +904,12 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
739
904
  lines.push(`import 'package:${pkg}/core/home_widgets/home_widget_service.dart';`);
740
905
  }
741
906
  lines.push(`import 'package:${pkg}/core/initializer/onstart_widget.dart';`);
907
+ lines.push(`import 'package:${pkg}/core/keyboard_fix/keyboard_flicker_fix.dart';`);
742
908
  lines.push(`import 'package:${pkg}/core/shared_preferences/shared_preferences.dart';`);
743
909
  lines.push(`import 'package:${pkg}/core/states/user_state_notifier.dart';`);
910
+ lines.push(`import 'package:${pkg}/core/dev_inspector/dev_inspector.dart';`);
744
911
  lines.push(`import 'package:${pkg}/core/theme/theme.dart';`);
912
+ lines.push(`import 'package:${pkg}/core/web_device_preview/web_device_preview.dart';`);
745
913
  lines.push(`import 'package:${pkg}/environnements.dart';`);
746
914
  if (withFirebase) {
747
915
  lines.push(`import 'package:${pkg}/firebase_options_dev.dart' as firebase_dev;`);
@@ -749,6 +917,9 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
749
917
  lines.push(`import 'package:${pkg}/i18n/translations.g.dart';`);
750
918
  lines.push(`import 'package:${pkg}/features/authentication/api/authentication_api.dart';`);
751
919
  lines.push(`import 'package:${pkg}/features/notifications/api/local_notifier.dart';`);
920
+ if (withFirebase) {
921
+ lines.push(`import 'package:${pkg}/features/notifications/repositories/background_notification_handler.dart';`);
922
+ }
752
923
  lines.push(`import 'package:${pkg}/features/notifications/repositories/notifications_repository.dart';`);
753
924
  if (withRevenuecat) {
754
925
  lines.push(`import 'package:${pkg}/features/subscription/repositories/subscription_repository.dart';`);
@@ -765,7 +936,8 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
765
936
 
766
937
  // ── main() ────────────────────────────────────────────────────────────────
767
938
  lines.push(`void main() async {`);
768
- lines.push(` WidgetsFlutterBinding.ensureInitialized();`);
939
+ lines.push(` final widgetsBinding = KasyBinding.ensureInitialized();`);
940
+ lines.push(` FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);`);
769
941
  lines.push(` await AppEnv.load();`);
770
942
  lines.push(` final env = Environment.fromEnv();`);
771
943
  lines.push(` final logger = Logger();`);
@@ -812,11 +984,21 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
812
984
  lines.push(` options: firebase_dev.DefaultFirebaseOptions.currentPlatform,`);
813
985
  lines.push(` ),`);
814
986
  lines.push(` ProdEnvironment() => Firebase.initializeApp(`);
815
- lines.push(` // TODO replace with your own firebase options for production environment (if needed)`);
987
+ lines.push(` // SETUP REQUIRED: For production, create a separate Firebase project and run:`);
988
+ lines.push(` // flutterfire configure --project=<your-prod-project> --out=lib/firebase_options_prod.dart`);
989
+ lines.push(` // Then import firebase_options_prod.dart and use it here instead.`);
990
+ lines.push(` // Reusing the dev project in production is only safe for early-stage development.`);
816
991
  lines.push(` options: firebase_dev.DefaultFirebaseOptions.currentPlatform,`);
817
992
  lines.push(` ),`);
818
993
  lines.push(` };`);
819
994
  lines.push('');
995
+ lines.push(` // Jiffy locale must be set AFTER Firebase init because Firebase resets`);
996
+ lines.push(` // Intl.defaultLocale internally, which would override our setting.`);
997
+ lines.push(` await Jiffy.setLocale(LocaleSettings.instance.currentLocale.languageCode);`);
998
+ lines.push('');
999
+ lines.push(` // MUST be registered at the top-level BEFORE runApp().`);
1000
+ lines.push(` FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);`);
1001
+ lines.push('');
820
1002
  }
821
1003
 
822
1004
  if (withSentry) {
@@ -859,73 +1041,102 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
859
1041
  lines.push(`// mode: ThemeMode.dark,`);
860
1042
  lines.push(`// ),`);
861
1043
  lines.push(`// See ./docs/theme.md for more details`);
862
- lines.push(`class MyApp extends ConsumerWidget {`);
1044
+ lines.push(`class MyApp extends ConsumerStatefulWidget {`);
863
1045
  lines.push(` final SharedPreferences sharedPreferences;`);
864
1046
  lines.push('');
865
1047
  lines.push(` const MyApp({super.key, required this.sharedPreferences});`);
866
1048
  lines.push('');
1049
+ lines.push(` @override`);
1050
+ lines.push(` ConsumerState<MyApp> createState() => _MyAppState();`);
1051
+ lines.push(`}`);
1052
+ lines.push('');
1053
+ lines.push(`class _MyAppState extends ConsumerState<MyApp> {`);
1054
+ lines.push(` late final AppTheme _appTheme;`);
1055
+ lines.push('');
1056
+ lines.push(` @override`);
1057
+ lines.push(` void initState() {`);
1058
+ lines.push(` super.initState();`);
1059
+ lines.push(` _appTheme = AppTheme.uniform(`);
1060
+ lines.push(` sharedPreferences: widget.sharedPreferences,`);
1061
+ lines.push(` themeFactory: const UniversalThemeFactory(),`);
1062
+ lines.push(` lightColors: KasyColors.light(),`);
1063
+ lines.push(` darkColors: KasyColors.dark(),`);
1064
+ lines.push(` textTheme: KasyTextTheme.build(),`);
1065
+ lines.push(` defaultMode: ThemeMode.system,`);
1066
+ lines.push(` );`);
1067
+ lines.push(` }`);
1068
+ lines.push('');
1069
+ lines.push(` @override`);
1070
+ lines.push(` void dispose() {`);
1071
+ lines.push(` _appTheme.dispose();`);
1072
+ lines.push(` super.dispose();`);
1073
+ lines.push(` }`);
1074
+ lines.push('');
867
1075
  lines.push(` // This widget is the root of your application.`);
868
1076
  lines.push(` @override`);
869
- lines.push(` Widget build(BuildContext context, WidgetRef ref) {`);
1077
+ lines.push(` Widget build(BuildContext context) {`);
870
1078
  lines.push(` ErrorWidget.builder = (FlutterErrorDetails details) {`);
871
1079
  lines.push(` return AppErrorWidget(error: details);`);
872
1080
  lines.push(` };`);
873
1081
  lines.push(` final goRouter = ref.watch(goRouterProvider);`);
874
1082
  lines.push('');
875
1083
  lines.push(` return ThemeProvider(`);
876
- lines.push(` notifier: AppTheme.uniform(`);
877
- lines.push(` sharedPreferences: sharedPreferences,`);
878
- lines.push(` themeFactory: const UniversalThemeFactory(),`);
879
- lines.push(` lightColors: KasyColors.light(),`);
880
- lines.push(` darkColors: KasyColors.dark(),`);
881
- lines.push(` textTheme: KasyTextTheme.build(),`);
882
- lines.push(` defaultMode: ThemeMode.light,`);
883
- lines.push(` ),`);
1084
+ lines.push(` notifier: _appTheme,`);
884
1085
  lines.push(` child: Builder(`);
885
1086
  lines.push(` builder: (context) {`);
886
- lines.push(` return MaterialApp.router(`);
887
- lines.push(` title: 'Kasy',`);
888
- lines.push(` theme: ThemeProvider.of(context).light,`);
889
- lines.push(` darkTheme: ThemeProvider.of(context).dark,`);
890
- lines.push(` themeMode: ThemeProvider.of(context).mode,`);
891
- lines.push(` routerConfig: goRouter,`);
892
- lines.push(` localizationsDelegates: const [`);
893
- lines.push(` GlobalMaterialLocalizations.delegate,`);
894
- lines.push(` GlobalWidgetsLocalizations.delegate,`);
895
- lines.push(` GlobalCupertinoLocalizations.delegate,`);
896
- lines.push(` ],`);
897
- lines.push(` locale: TranslationProvider.of(context).flutterLocale,`);
898
- lines.push(` supportedLocales: AppLocaleUtils.supportedLocales,`);
899
- lines.push(` // Initializer is a widget that allows us to run some code before the app is ready`);
900
- lines.push(` builder: (context, child) => Initializer(`);
901
- lines.push(` services: [`);
902
- lines.push(` authenticationApiProvider,`);
903
- lines.push(` // shared preferences must be loaded`);
904
- lines.push(` sharedPreferencesProvider,`);
905
- lines.push(` // remote config api`);
906
- lines.push(` remoteConfigApiProvider,`);
907
- lines.push(` // notifications`);
908
- lines.push(` notificationsSettingsProvider,`);
909
- lines.push(` notificationRepositoryProvider,`);
910
- lines.push(` // user state`);
1087
+ // WebDevicePreview + DevInspector wrap the app so the in-app dev tools work:
1088
+ // • Cmd/Ctrl + Shift + P → widget inspector
1089
+ // • Cmd/Ctrl + Shift + D → device preview (web)
1090
+ // Both are no-ops in release builds.
1091
+ lines.push(` return WebDevicePreview.wrap(`);
1092
+ lines.push(` child: DevInspector.wrap(`);
1093
+ lines.push(` child: MaterialApp.router(`);
1094
+ lines.push(` title: 'Kasy',`);
1095
+ lines.push(` scaffoldMessengerKey: devInspectorRootScaffoldMessengerKey,`);
1096
+ lines.push(` theme: ThemeProvider.of(context).light,`);
1097
+ lines.push(` darkTheme: ThemeProvider.of(context).dark,`);
1098
+ lines.push(` themeMode: ThemeProvider.of(context).mode,`);
1099
+ lines.push(` themeAnimationDuration: Duration.zero,`);
1100
+ lines.push(` routerConfig: goRouter,`);
1101
+ lines.push(` localizationsDelegates: const [`);
1102
+ lines.push(` GlobalMaterialLocalizations.delegate,`);
1103
+ lines.push(` GlobalWidgetsLocalizations.delegate,`);
1104
+ lines.push(` GlobalCupertinoLocalizations.delegate,`);
1105
+ lines.push(` ],`);
1106
+ lines.push(` locale: TranslationProvider.of(context).flutterLocale,`);
1107
+ lines.push(` supportedLocales: AppLocaleUtils.supportedLocales,`);
1108
+ lines.push(` // Initializer is a widget that allows us to run some code before the app is ready`);
1109
+ lines.push(` builder: (context, child) => Initializer(`);
1110
+ lines.push(` services: [`);
1111
+ lines.push(` authenticationApiProvider,`);
1112
+ lines.push(` // shared preferences must be loaded`);
1113
+ lines.push(` sharedPreferencesProvider,`);
1114
+ lines.push(` // remote config api`);
1115
+ lines.push(` remoteConfigApiProvider,`);
1116
+ lines.push(` // notifications`);
1117
+ lines.push(` notificationsSettingsProvider,`);
1118
+ lines.push(` notificationRepositoryProvider,`);
1119
+ lines.push(` // user state`);
911
1120
  if (withRevenuecat) {
912
- lines.push(` subscriptionRepositoryProvider,`);
1121
+ lines.push(` subscriptionRepositoryProvider,`);
913
1122
  }
914
- lines.push(` userStateNotifierProvider.notifier,`);
1123
+ lines.push(` userStateNotifierProvider.notifier,`);
915
1124
  if (withWidget) {
916
- lines.push(` homeWidgetsManagerProvider,`);
1125
+ lines.push(` homeWidgetsManagerProvider,`);
917
1126
  }
918
- lines.push(` // analytics`);
919
- lines.push(` analyticsApiProvider,`);
920
- lines.push(` facebookEventApiProvider,`);
921
- lines.push(` ],`);
922
- lines.push(` onReady: child!,`);
923
- lines.push(` onError: (_, error) => InitializationErrorPage(error: error),`);
924
- lines.push(` onLoading: Scaffold(`);
925
- lines.push(` body: Center(`);
926
- lines.push(` child: CircularProgressIndicator.adaptive(`);
927
- lines.push(` valueColor: AlwaysStoppedAnimation<Color>(`);
928
- lines.push(` context.colors.primary,`);
1127
+ lines.push(` // analytics`);
1128
+ lines.push(` analyticsApiProvider,`);
1129
+ lines.push(` facebookEventApiProvider,`);
1130
+ lines.push(` ],`);
1131
+ lines.push(` onReady: DevicePreview.appBuilder(context, child),`);
1132
+ lines.push(` onError: (_, error) => InitializationErrorPage(error: error),`);
1133
+ lines.push(` onLoading: Scaffold(`);
1134
+ lines.push(` body: Center(`);
1135
+ lines.push(` child: CircularProgressIndicator.adaptive(`);
1136
+ lines.push(` valueColor: AlwaysStoppedAnimation<Color>(`);
1137
+ lines.push(` context.colors.primary,`);
1138
+ lines.push(` ),`);
1139
+ lines.push(` ),`);
929
1140
  lines.push(` ),`);
930
1141
  lines.push(` ),`);
931
1142
  lines.push(` ),`);
@@ -1149,6 +1360,30 @@ class AdminHomeWidgets extends StatelessWidget {
1149
1360
  );
1150
1361
  }
1151
1362
 
1363
+ /**
1364
+ * Strips the home-widget hook from language_switcher.dart when the widget module
1365
+ * is not selected. The switcher pushes the new locale to the home widget via
1366
+ * myWidgetHomeWidgetProvider, which only exists with the widget module — without
1367
+ * this the generated project fails to compile (undefined provider + dead import).
1368
+ *
1369
+ * @param {string} projectDir
1370
+ */
1371
+ async function patchLanguageSwitcherNoWidget(projectDir) {
1372
+ const filePath = path.join(
1373
+ projectDir, 'lib', 'features', 'settings', 'ui', 'components', 'language_switcher.dart',
1374
+ );
1375
+ if (!(await fs.pathExists(filePath))) return;
1376
+ let content = await fs.readFile(filePath, 'utf8');
1377
+ // Drop the home-widget service import.
1378
+ content = content.replace(/^import 'package:[^']*home_widget_mywidget_service\.dart';\n/m, '');
1379
+ // Drop the unawaited(...) block that updates the home widget on locale change.
1380
+ content = content.replace(
1381
+ /\n\s*unawaited\(\s*ref[\s\S]*?myWidgetHomeWidgetProvider[\s\S]*?\);/,
1382
+ '',
1383
+ );
1384
+ await fs.outputFile(filePath, content, 'utf8');
1385
+ }
1386
+
1152
1387
  /**
1153
1388
  * Writes a NoOp feature_request_repository.dart when the feedback module is not selected.
1154
1389
  * premium_page_provider.dart imports this provider — without it the project won't compile.
@@ -1250,7 +1485,12 @@ async function writeNoOpSentryUsages(projectDir) {
1250
1485
  if (!(await fs.pathExists(filePath))) continue;
1251
1486
  let content = await fs.readFile(filePath, 'utf8');
1252
1487
  content = content.replace(sentryImport, '');
1253
- content = content.replace(/^[ \t]*Sentry\.captureException\([^)]*\);\n/gm, '');
1488
+ // Replace the Sentry call with a comment (not nothing) so the catch block
1489
+ // isn't left empty — an empty catch trips the empty_catches lint.
1490
+ content = content.replace(
1491
+ /^([ \t]*)Sentry\.captureException\([^)]*\);\n/gm,
1492
+ '$1// Error not reported — Sentry is disabled. Run: kasy add sentry.\n',
1493
+ );
1254
1494
  await fs.outputFile(filePath, content, 'utf8');
1255
1495
  }
1256
1496
  }
@@ -1430,9 +1670,13 @@ sealed class Subscription with _$Subscription {
1430
1670
  }
1431
1671
 
1432
1672
  // 1. No-op subscription_repository.dart (used by user_repository, onboarding, home_widget_background_task)
1673
+ // The constructor keeps subscriptionApi/inAppSubscriptionApi/prefs params (unused
1674
+ // here) for source-compat with the bundled tests. ignore_for_file stops
1675
+ // `dart fix` from stripping them via avoid_unused_constructor_parameters.
1433
1676
  await fs.outputFile(
1434
1677
  path.join(projectDir, 'lib', 'features', 'subscription', 'repositories', 'subscription_repository.dart'),
1435
- `import 'package:flutter_riverpod/flutter_riverpod.dart';
1678
+ `// ignore_for_file: avoid_unused_constructor_parameters
1679
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
1436
1680
  import 'package:${pkg}/core/data/models/subscription.dart';
1437
1681
  import 'package:${pkg}/core/initializer/onstart_service.dart';
1438
1682
  // No-op subscription repository. Run: kasy add revenuecat to activate.
@@ -1525,7 +1769,8 @@ enum PaywallFactory { basic, basicRow, minimal, withSwitch }
1525
1769
  // 5. No-op subscription_entity.dart (used by test_utils, fake_subscription_api)
1526
1770
  await fs.outputFile(
1527
1771
  path.join(projectDir, 'lib', 'features', 'subscription', 'api', 'entities', 'subscription_entity.dart'),
1528
- `// No-op subscription entity. Run: kasy add revenuecat to activate.
1772
+ `// ignore_for_file: constant_identifier_names
1773
+ // No-op subscription entity. Run: kasy add revenuecat to activate.
1529
1774
  enum SubscriptionStatus { ACTIVE, PAUSED, EXPIRED, LIFETIME, CANCELLED }
1530
1775
 
1531
1776
  class SubscriptionEntity {
@@ -1687,6 +1932,7 @@ module.exports = {
1687
1932
  writeEnvExample,
1688
1933
  writeEnvFileIfMissing,
1689
1934
  writeFirebaserc,
1935
+ cleanFirebaseJsonKitRefs,
1690
1936
  writeEnvironnementsOverrides,
1691
1937
  writeFeaturesConfig,
1692
1938
  writeKitSetup,
@@ -1699,6 +1945,7 @@ module.exports = {
1699
1945
  removeFacebookSigninFromAuthPages,
1700
1946
  removeAndroidFacebookMetadata,
1701
1947
  writeNoOpAdminHomeWidgets,
1948
+ patchLanguageSwitcherNoWidget,
1702
1949
  writeNoOpFeatureRequestRepository,
1703
1950
  writeNoOpSubscriptionStubs,
1704
1951
  writeNoOpSentryUsages,
@@ -864,6 +864,16 @@ async function getGoogleClientSecretViaGcloud(firebaseProjectId) {
864
864
  }
865
865
  }
866
866
 
867
+ // Deploys only Firestore security rules so the generated app works immediately
868
+ // without needing `kasy deploy`. Fast (<30s) and billing-free.
869
+ async function deployFirestoreRules(projectDir, firebaseProjectId) {
870
+ return run(
871
+ `firebase deploy --only firestore:rules --project ${firebaseProjectId}`,
872
+ projectDir,
873
+ 120_000, // 2 min
874
+ );
875
+ }
876
+
867
877
  module.exports = {
868
878
  pubGet,
869
879
  slangGenerate,
@@ -880,6 +890,7 @@ module.exports = {
880
890
  validateFacebookAndroidStrings,
881
891
  validateRevenueCat,
882
892
  patchFirebaseServiceWorker,
893
+ deployFirestoreRules,
883
894
  readSupabaseGoogleCredentials,
884
895
  getGoogleClientSecretViaGcloud,
885
896
  // Exported for focused unit tests; not part of the public CLI surface.
@@ -524,7 +524,7 @@ module.exports = {
524
524
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
525
525
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
526
526
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
527
- 'new.firebase.module.web': '🌐 Web Support (PWA, Firebase only)',
527
+ 'new.firebase.module.web': '🌐 Web Support (PWA)',
528
528
  'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
529
529
  'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
530
530
  'new.firebase.module.local_notifications': '🔔 Local Reminders (no server)',
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Checking Blaze status...',
634
634
  'new.firebase.interactive.billingTimeout': 'Blaze plan not confirmed after timeout. Deploy skipped — run manually when ready.',
635
635
  'new.firebase.interactive.authWarn': 'Could not enable Email/Password and Anonymous auth automatically. Enable manually:',
636
+ 'new.firebase.localhostWarn': 'Could not authorize localhost for web sign-in. To test login in the browser, add "localhost" under Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'Could not activate APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: enable manually in Authentication → Sign-in method → Google',
638
639
 
@@ -691,6 +692,10 @@ module.exports = {
691
692
  'new.google.refreshConfigs': 'Updating google-services.json and GoogleService-Info.plist with Google Client IDs…',
692
693
  'new.google.manualHint': 'Google Sign-In: enable manually in the Console (Google provider):',
693
694
  'new.google.manualHint.noEmail': 'Google Sign-In: could not detect a support email (gcloud has no account). Enable manually in the Console:',
695
+ 'new.google.supabaseManual': 'Google Sign-In: client created, but the secret was not available yet. Enable it later in the Supabase dashboard (Authentication > Providers > Google).',
696
+ 'new.fcm.ok': 'generated automatically',
697
+ 'new.fcm.failSupabase': 'not generated (GCP permission still propagating); set FIREBASE_SERVICE_ACCOUNT_JSON in your Supabase secrets',
698
+ 'new.fcm.failApi': 'not generated (GCP permission still propagating); run the command again in a few minutes',
694
699
  'new.sha1.registering': 'Registering SHA-1 for Google Sign-In (Android)…',
695
700
  'new.sha1.failed': 'SHA-1 not added automatically: {error}',
696
701
  'new.sha1.manual': 'Add it manually so Google Sign-In works on Android:',
@@ -524,7 +524,7 @@ module.exports = {
524
524
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
525
525
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
526
526
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
527
- 'new.firebase.module.web': '🌐 Web Support (PWA, solo Firebase)',
527
+ 'new.firebase.module.web': '🌐 Web Support (PWA)',
528
528
  'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
529
529
  'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
530
530
  'new.firebase.module.local_notifications': '🔔 Local Reminders (sin servidor)',
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Verificando estado del Blaze...',
634
634
  'new.firebase.interactive.billingTimeout': 'Plan Blaze no confirmado tras el tiempo límite. Despliegue omitido — ejecuta manualmente cuando estés listo.',
635
635
  'new.firebase.interactive.authWarn': 'No se pudo activar Email/Contraseña y Anónimo automáticamente. Actívalos manualmente:',
636
+ 'new.firebase.localhostWarn': 'No se pudo autorizar localhost para el inicio de sesión web. Para probar el login en el navegador, agrega "localhost" en Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'No se pudieron activar las APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: activa manualmente en Authentication → Sign-in method → Google',
638
639
 
@@ -691,6 +692,10 @@ module.exports = {
691
692
  'new.google.refreshConfigs': 'Actualizando google-services.json y GoogleService-Info.plist con los Client IDs de Google…',
692
693
  'new.google.manualHint': 'Inicio de sesión con Google: actívalo manualmente en la consola (proveedor Google):',
693
694
  'new.google.manualHint.noEmail': 'Inicio de sesión con Google: no detecté un email de soporte (gcloud sin cuenta). Actívalo manualmente en la consola:',
695
+ 'new.google.supabaseManual': 'Inicio de sesión con Google: cliente creado, pero el secret aún no estaba disponible. Actívalo luego en el panel de Supabase (Authentication > Providers > Google).',
696
+ 'new.fcm.ok': 'generada automáticamente',
697
+ 'new.fcm.failSupabase': 'no generada (permiso de GCP aún propagando); define FIREBASE_SERVICE_ACCOUNT_JSON en los secrets de Supabase',
698
+ 'new.fcm.failApi': 'no generada (permiso de GCP aún propagando); ejecuta el comando de nuevo en unos minutos',
694
699
  'new.sha1.registering': 'Registrando SHA-1 para Google Sign-In (Android)…',
695
700
  'new.sha1.failed': 'SHA-1 no añadido automaticamente: {error}',
696
701
  'new.sha1.manual': 'Agregalo manualmente para que Google Sign-In funcione en Android:',
@@ -524,7 +524,7 @@ module.exports = {
524
524
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
525
525
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
526
526
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
527
- 'new.firebase.module.web': '🌐 Web Support (PWA, somente Firebase)',
527
+ 'new.firebase.module.web': '🌐 Web Support (PWA)',
528
528
  'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
529
529
  'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
530
530
  'new.firebase.module.local_notifications': '🔔 Local Reminders (sem servidor)',
@@ -633,6 +633,7 @@ module.exports = {
633
633
  'new.firebase.interactive.billingWaiting': 'Verificando status do Blaze...',
634
634
  'new.firebase.interactive.billingTimeout': 'Plano Blaze não confirmado apos o tempo limite. Deploy ignorado — rode manualmente quando estiver pronto.',
635
635
  'new.firebase.interactive.authWarn': 'Não foi possível ativar Email/Senha e Anônimo automaticamente. Ative manualmente:',
636
+ 'new.firebase.localhostWarn': 'Não foi possível autorizar localhost para login web. Se for testar login no navegador, adicione "localhost" em Authorized domains:',
636
637
  'new.firebase.existing.apisFailed': 'Não foi possível ativar APIs:',
637
638
  'new.firebase.existing.googleSignInManual': 'Google Sign-In: ative manualmente em Authentication → Sign-in method → Google',
638
639
 
@@ -691,6 +692,10 @@ module.exports = {
691
692
  'new.google.refreshConfigs': 'Atualizando google-services.json e GoogleService-Info.plist com Client IDs do Google…',
692
693
  'new.google.manualHint': 'Login com Google: ative manualmente no Console (provedor Google):',
693
694
  'new.google.manualHint.noEmail': 'Login com Google: não consegui detectar um e-mail de suporte (gcloud sem conta). Ative manualmente no Console:',
695
+ 'new.google.supabaseManual': 'Login com Google: client criado, mas o secret ainda não estava disponível. Ative depois no painel do Supabase (Authentication > Providers > Google).',
696
+ 'new.fcm.ok': 'gerada automaticamente',
697
+ 'new.fcm.failSupabase': 'não gerada (permissão do GCP ainda propagando); defina FIREBASE_SERVICE_ACCOUNT_JSON nos secrets do Supabase',
698
+ 'new.fcm.failApi': 'não gerada (permissão do GCP ainda propagando); rode o comando de novo em alguns minutos',
694
699
  'new.sha1.registering': 'Registrando SHA-1 para Google Sign-In (Android)…',
695
700
  'new.sha1.failed': 'SHA-1 não adicionado automaticamente: {error}',
696
701
  'new.sha1.manual': 'Adicione manualmente para o Google Sign-In funcionar no Android:',