kasy-cli 1.34.0 → 1.36.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.
Files changed (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +18 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +215 -178
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +226 -105
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
@@ -0,0 +1,66 @@
1
+ {
2
+ "api/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
3
+ "api/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
4
+ "api/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
5
+ "api/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
6
+ "api/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
7
+ "api/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
8
+ "api/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
9
+ "api/lib/features/ai_chat/api/ai_chat_api.dart": "749047d30390e8d624c5a17d421096b05651e28b34a42071a9b73efc71d0a5f7",
10
+ "api/lib/features/ai_chat/api/ai_chat_conversation_entity.dart": "1c4e4223e802d7fe8e93d9e7a09fcc6be81a41792d722e8b7e697837cde4737f",
11
+ "api/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
12
+ "api/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
13
+ "api/lib/features/authentication/api/authentication_api_interface.dart": "ef9219237babd73e4b3ac148a24ebbbdf39ab98166d21f578c359074b528e5da",
14
+ "api/lib/features/authentication/api/authentication_api.dart": "8b460722bb3bee7eee015f34aaa39c1c97cb264df1024b2855060dac232c620a",
15
+ "api/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
16
+ "api/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
17
+ "api/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
18
+ "api/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
19
+ "api/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
20
+ "api/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
21
+ "api/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
22
+ "api/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
23
+ "api/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
24
+ "api/lib/features/onboarding/api/entities/user_info_entity.dart": "cd5aabc68310025f3ab9676cab126e1f755431f9e8752f868ee9fcd6f5892ce2",
25
+ "api/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
26
+ "api/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
27
+ "api/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
28
+ "api/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
29
+ "api/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
30
+ "api/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
31
+ "api/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
32
+ "api/lib/features/subscriptions/api/subscription_api.dart": "c7484c9301d16245748025a3d420a89397e9e956b2211b7ed001c073ff2e4449",
33
+ "api/README.md": "1f30fc1ebf8fe02df6f3ee2f94c17f4bb4952abd5b125312d2c43eb6374eb354",
34
+ "supabase/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
35
+ "supabase/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
36
+ "supabase/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
37
+ "supabase/lib/core/data/api/user_api.dart": "06f491ebf4548b87431e2d6983148f4082e120ec7d6a928108cba954d16d8c56",
38
+ "supabase/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
39
+ "supabase/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
40
+ "supabase/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
41
+ "supabase/lib/features/ai_chat/api/ai_chat_api.dart": "749047d30390e8d624c5a17d421096b05651e28b34a42071a9b73efc71d0a5f7",
42
+ "supabase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart": "1c4e4223e802d7fe8e93d9e7a09fcc6be81a41792d722e8b7e697837cde4737f",
43
+ "supabase/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
44
+ "supabase/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
45
+ "supabase/lib/features/authentication/api/authentication_api.dart": "8b460722bb3bee7eee015f34aaa39c1c97cb264df1024b2855060dac232c620a",
46
+ "supabase/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
47
+ "supabase/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
48
+ "supabase/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
49
+ "supabase/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
50
+ "supabase/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
51
+ "supabase/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
52
+ "supabase/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
53
+ "supabase/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
54
+ "supabase/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
55
+ "supabase/lib/features/onboarding/api/entities/user_info_entity.dart": "cd5aabc68310025f3ab9676cab126e1f755431f9e8752f868ee9fcd6f5892ce2",
56
+ "supabase/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
57
+ "supabase/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
58
+ "supabase/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
59
+ "supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
60
+ "supabase/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
61
+ "supabase/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
62
+ "supabase/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
63
+ "supabase/lib/features/subscriptions/api/subscription_api.dart": "c7484c9301d16245748025a3d420a89397e9e956b2211b7ed001c073ff2e4449",
64
+ "supabase/lib/google_auth_options.dart": "9d3ab9be5928f3100b9b217864d916edd1223d186e60ed130c35628812cace66",
65
+ "supabase/README.md": "1f30fc1ebf8fe02df6f3ee2f94c17f4bb4952abd5b125312d2c43eb6374eb354"
66
+ }
@@ -101,11 +101,13 @@ Deno.serve(async (req: Request) => {
101
101
  const item = sub.items?.data?.[0];
102
102
  const priceId = item?.price?.id ?? "";
103
103
  const ms = periodEndMs(sub);
104
+ const trialMs = sub.trial_end ? sub.trial_end * 1000 : null;
104
105
  const now = new Date().toISOString();
105
106
  const payload = {
106
107
  status: statusFromStripe(sub),
107
108
  last_update_date: now,
108
109
  period_end_date: ms ? new Date(ms).toISOString() : null,
110
+ trial_end: trialMs ? new Date(trialMs).toISOString() : null,
109
111
  sku_id: priceId,
110
112
  offer_id: priceId,
111
113
  store: "STRIPE",
@@ -1,35 +1,36 @@
1
- -- Send a welcome notification when the user's first device is registered.
1
+ -- Send a welcome notification ONCE per account when a device is registered.
2
2
  --
3
3
  -- Why on device registration and not on user creation?
4
- -- At user creation time no device token exists yet. By firing on the first
5
- -- INSERT into `devices` we guarantee the locale is set and the notification
6
- -- can be localised accurately.
4
+ -- At user creation time no device token exists yet. By firing on INSERT into
5
+ -- `devices` we guarantee the locale is set and the notification can be localised
6
+ -- accurately.
7
7
  --
8
- -- notify_user = false: the user is already inside the app at registration
9
- -- time, so no push is needed the notification is saved to the DB only.
8
+ -- Why a `welcome_sent` flag and not a device count?
9
+ -- Logout removes the device row and login re-creates it, so a device count
10
+ -- ("is this the first device?") sees a brand-new first device on every
11
+ -- re-login and re-sent the welcome each time. A one-shot flag on the user,
12
+ -- claimed atomically, sends it exactly once for the life of the account.
10
13
  --
11
- -- Anonymous users (no email in public.users) are skipped.
12
- -- The message is localised using the `locale` field of public.users.
14
+ -- notify_user = false: the user is already inside the app at registration time,
15
+ -- so no push is needed the notification is saved to the DB only.
16
+ --
17
+ -- Anonymous users (no email in public.users) are skipped until they have one.
18
+
19
+ -- One-time welcome guard (idempotent so re-running the migration is safe).
20
+ ALTER TABLE public.users
21
+ ADD COLUMN IF NOT EXISTS welcome_sent boolean NOT NULL DEFAULT false;
13
22
 
14
23
  CREATE OR REPLACE FUNCTION public.trigger_welcome_notification()
15
24
  RETURNS TRIGGER AS $$
16
25
  DECLARE
17
- v_email text;
18
- v_locale text;
19
- v_count int;
20
- v_title text;
21
- v_body text;
26
+ v_email text;
27
+ v_locale text;
28
+ v_claimed int;
29
+ v_title text;
30
+ v_body text;
22
31
  BEGIN
23
- -- Only fire for the very first device of this user.
24
- SELECT COUNT(*) INTO v_count
25
- FROM public.devices
26
- WHERE user_id = NEW.user_id AND id != NEW.id;
27
-
28
- IF v_count > 0 THEN
29
- RETURN NEW;
30
- END IF;
31
-
32
- -- Skip anonymous users (no email set yet).
32
+ -- Skip anonymous users (no email yet); they get welcomed once they have one,
33
+ -- WITHOUT consuming the one-time claim below.
33
34
  SELECT email, locale INTO v_email, v_locale
34
35
  FROM public.users
35
36
  WHERE id = NEW.user_id;
@@ -38,6 +39,18 @@ BEGIN
38
39
  RETURN NEW;
39
40
  END IF;
40
41
 
42
+ -- Atomically claim the one-time welcome: flip welcome_sent to true only if it
43
+ -- isn't already. ROW_COUNT is 1 for the very first claim, 0 for any later
44
+ -- device registration — including one re-created after a logout/login cycle.
45
+ UPDATE public.users
46
+ SET welcome_sent = true
47
+ WHERE id = NEW.user_id AND welcome_sent = false;
48
+
49
+ GET DIAGNOSTICS v_claimed = ROW_COUNT;
50
+ IF v_claimed = 0 THEN
51
+ RETURN NEW;
52
+ END IF;
53
+
41
54
  -- Localised welcome message (falls back to English).
42
55
  IF v_locale = 'pt' THEN
43
56
  v_title := 'Bem-vindo!';
@@ -0,0 +1,6 @@
1
+ -- Adds trial_end to subscriptions so trial periods are persisted and the app
2
+ -- can detect an active trial. Mirrors the Firebase backend, where the Stripe
3
+ -- webhook writes trial_end (RevenueCat trials are read from the SDK, not the DB).
4
+ -- Idempotent: safe to run on a fresh project or an already-deployed database.
5
+ ALTER TABLE public.subscriptions
6
+ ADD COLUMN IF NOT EXISTS trial_end TIMESTAMPTZ;
@@ -1,3 +1,4 @@
1
+ import 'dart:async';
1
2
  import 'dart:convert';
2
3
  import 'package:crypto/crypto.dart';
3
4
  import 'package:firebase_auth/firebase_auth.dart' as fb_auth;
@@ -11,6 +12,8 @@ import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
11
12
  import 'package:kasy_kit/core/data/entities/user_entity.dart';
12
13
  import 'package:kasy_kit/environments.dart';
13
14
  import 'package:kasy_kit/google_auth_options.dart';
15
+ import 'package:kasy_kit/features/authentication/api/auth_web_support.dart'
16
+ if (dart.library.js_interop) 'package:kasy_kit/features/authentication/api/auth_web_support_web.dart';
14
17
  import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
15
18
  import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
16
19
  import 'package:sign_in_with_apple/sign_in_with_apple.dart';
@@ -34,7 +37,41 @@ class SupabaseAuthenticationApi implements AuthenticationApi {
34
37
  });
35
38
 
36
39
  @override
37
- Future<void> init() async {}
40
+ Future<void> init() async {
41
+ if (!kIsWeb) return;
42
+ // Complete a pending mobile-web Google redirect sign-in. Firebase handled the
43
+ // full-page redirect (we reuse its web OAuth client to mint the Google ID
44
+ // token — see [signinWithGoogle]), so on return we exchange that token for a
45
+ // Supabase session. No-op when no redirect is pending or a session already
46
+ // exists.
47
+ try {
48
+ final result = await fb_auth.FirebaseAuth.instance.getRedirectResult();
49
+ final idToken = (result.credential as fb_auth.OAuthCredential?)?.idToken;
50
+ if (idToken != null && client.auth.currentUser == null) {
51
+ await client.auth.signInWithIdToken(
52
+ provider: OAuthProvider.google,
53
+ idToken: idToken,
54
+ );
55
+ }
56
+ } catch (e) {
57
+ _logger.w('Google redirect sign-in completion failed: $e');
58
+ }
59
+ }
60
+
61
+ /// Mobile-web Google sign-in: start a full-page Firebase redirect instead of a
62
+ /// popup. Mobile browsers handle popups unreliably (the result is often lost
63
+ /// when the browser reclaims the backgrounded opener tab), leaving the app
64
+ /// stuck "signing in". The Supabase session is established at startup by [init]
65
+ /// via getRedirectResult. The page is navigating away as soon as
66
+ /// [signInWithRedirect] resolves, so this future intentionally never completes
67
+ /// — the caller stays in its loading state until the browser leaves.
68
+ /// See [isMobileWebBrowser].
69
+ Future<Credentials> _googleSignInWithRedirectWeb() async {
70
+ await fb_auth.FirebaseAuth.instance.signInWithRedirect(
71
+ fb_auth.GoogleAuthProvider(),
72
+ );
73
+ return Completer<Credentials>().future;
74
+ }
38
75
 
39
76
  @override
40
77
  Future<void> recoverPassword(String email) {
@@ -225,6 +262,7 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
225
262
  @override
226
263
  Future<Credentials> signinWithGoogle() async {
227
264
  if (kIsWeb) {
265
+ if (isMobileWebBrowser()) return _googleSignInWithRedirectWeb();
228
266
  // google_sign_in v7 can't do imperative auth on web. Get the Google ID token
229
267
  // via Firebase's popup (zero manual config — reuses the Firebase web OAuth
230
268
  // client + authorized domains, which the kit already sets up) and sign into
@@ -368,9 +406,21 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
368
406
  @override
369
407
  Future<Credentials> signupFromAnonymousWithGoogle() async {
370
408
  if (kIsWeb) {
371
- // Web: get the Google ID token via Firebase's popup (see signinWithGoogle) and
372
- // link it to the current anonymous Supabase user.
409
+ if (isMobileWebBrowser()) return _googleSignInWithRedirectWeb();
410
+ // Web: get the Google ID token via Firebase's popup (see signinWithGoogle).
373
411
  final idToken = await _googleIdTokenFromWebPopup();
412
+ // On web the app runs in authRequired mode, so there is usually no anonymous
413
+ // Supabase session to link to (the user state is just a local placeholder).
414
+ // Linking without a session sends the anon key, which has no `sub`, and Supabase
415
+ // rejects it with "missing sub claim". When there's no session, sign in normally
416
+ // — it creates the account from the Google id_token. Mirrors the Firebase backend.
417
+ if (client.auth.currentUser == null) {
418
+ final res = await client.auth.signInWithIdToken(
419
+ provider: OAuthProvider.google,
420
+ idToken: idToken,
421
+ );
422
+ return Credentials(id: res.user!.id, token: res.session?.accessToken ?? '');
423
+ }
374
424
  try {
375
425
  final response = await client.auth.linkIdentityWithIdToken(
376
426
  provider: OAuthProvider.google,
@@ -605,6 +655,35 @@ Note: wait a minute after enabling anonymous sign-in before trying again. It tak
605
655
  Future<String?> getCurrentUserDisplayName() async =>
606
656
  client.auth.currentUser?.userMetadata?['full_name'] as String?;
607
657
 
658
+ @override
659
+ Future<String?> getCurrentUserPhotoUrl() async {
660
+ final meta = client.auth.currentUser?.userMetadata;
661
+ return (meta?['avatar_url'] ?? meta?['picture']) as String?;
662
+ }
663
+
664
+ @override
665
+ Future<List<String>> getLinkedProviders() async {
666
+ // Supabase exposes the linked identities in app_metadata['providers'].
667
+ final raw = client.auth.currentUser?.appMetadata['providers'];
668
+ if (raw is! List) return const [];
669
+ return raw.map((p) => p.toString()).toList();
670
+ }
671
+
672
+ @override
673
+ Future<void> setPassword(String password) async {
674
+ // Supabase keeps the same user; sets/updates the password so a social-only
675
+ // account can also sign in with email + password.
676
+ await client.auth.updateUser(UserAttributes(password: password));
677
+ }
678
+
679
+ // Supabase auto-links identities with the same (confirmed) email on sign-in,
680
+ // so there is no manual-link step: nothing to offer and nothing to do.
681
+ @override
682
+ Future<List<String>> linkableSocialProviders() async => const [];
683
+
684
+ @override
685
+ Future<void> linkSocialProvider(String provider) => Future.value();
686
+
608
687
  String _normalizePhoneNumber(String phoneNumber) {
609
688
  String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
610
689
  if (!normalized.startsWith('+')) {
@@ -68,6 +68,22 @@ abstract class AuthenticationRepository {
68
68
  /// Returns the display name of the authenticated user.
69
69
  Future<String?> getCurrentUserDisplayName();
70
70
 
71
+ /// Returns the photo URL of the current user (e.g. Google picture), or null.
72
+ Future<String?> getCurrentUserPhotoUrl();
73
+
74
+ /// Returns all sign-in providers linked to the current account.
75
+ Future<List<String>> getLinkedProviders();
76
+
77
+ /// Sets or updates an email/password credential for the current user, so a
78
+ /// social-only account can also sign in with email + password.
79
+ Future<void> setPassword(String password);
80
+
81
+ /// Social providers the current user can still link to their account.
82
+ Future<List<String>> linkableSocialProviders();
83
+
84
+ /// Links a social provider to the current account.
85
+ Future<void> linkSocialProvider(String provider);
86
+
71
87
  /// Signin with Google Play Games account on Android
72
88
  Future<void> signinWithGooglePlayGames();
73
89
 
@@ -268,6 +284,26 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
268
284
  Future<String?> getCurrentUserDisplayName() =>
269
285
  _authenticationApi.getCurrentUserDisplayName();
270
286
 
287
+ @override
288
+ Future<String?> getCurrentUserPhotoUrl() =>
289
+ _authenticationApi.getCurrentUserPhotoUrl();
290
+
291
+ @override
292
+ Future<List<String>> getLinkedProviders() =>
293
+ _authenticationApi.getLinkedProviders();
294
+
295
+ @override
296
+ Future<void> setPassword(String password) =>
297
+ _authenticationApi.setPassword(password);
298
+
299
+ @override
300
+ Future<List<String>> linkableSocialProviders() =>
301
+ _authenticationApi.linkableSocialProviders();
302
+
303
+ @override
304
+ Future<void> linkSocialProvider(String provider) =>
305
+ _authenticationApi.linkSocialProvider(provider);
306
+
271
307
  @override
272
308
  Future<void> signinWithGooglePlayGames() async {
273
309
  try {
@@ -37,6 +37,7 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
37
37
  @JsonKey(name: 'creation_date') DateTime? creationDate,
38
38
  @JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
39
39
  @JsonKey(name: 'period_end_date') DateTime? periodEndDate,
40
+ @JsonKey(name: 'trial_end') DateTime? trialEnd,
40
41
  @JsonKey(name: 'status') required SubscriptionStatus status,
41
42
  @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
42
43
  SubscriptionStore? store,
@@ -86,11 +86,50 @@ const { FIREBASE_SOURCE_DIR } = require('./shared/backend-config');
86
86
  * @param {object} [options.moduleAnswers={}]
87
87
  * @param {Function} [options.onProgress] - Callback to update the spinner in the CLI.
88
88
  * @param {object} [options.deploy] - Used by the Firebase postBuild hook.
89
+ * @param {boolean} [options.offline=false] - Validation mode: never touch the
90
+ * network or the live Firebase project. Skips flutterfire configure, the Google
91
+ * auth/iOS/SW patches it feeds, and the Firestore rules deploy; writes a
92
+ * compilable stub for firebase_options_dev.dart so `flutter analyze` still works.
89
93
  * @param {object} [hooks={}]
90
94
  * @param {Function|null} [hooks.applyBackendSetup]
91
95
  * @param {Function|null} [hooks.postBuild]
92
96
  * @returns {Promise<{steps: object[], packageName: string, appName: string, bundleId: string, firebaseProjectId: string}>}
93
97
  */
98
+
99
+ /**
100
+ * Writes a compilable placeholder for lib/firebase_options_dev.dart, the file
101
+ * `flutterfire configure` normally generates from the real Firebase project.
102
+ *
103
+ * Used only by offline validation (`npm run validate:backends`): every backend's
104
+ * main.dart imports this file (Firebase powers FCM/remote config everywhere), so
105
+ * without it `flutter analyze` fails with "uri doesn't exist". The stub exposes
106
+ * the single member main.dart uses — DefaultFirebaseOptions.currentPlatform — with
107
+ * dummy values, so the analyzer type-checks the whole project without any network.
108
+ */
109
+ async function writeFirebaseOptionsDevStub(targetDir) {
110
+ const stub = `// Offline validation stub written by \`npm run validate:backends\`.
111
+ // NOT shipped to clients: \`kasy new\` runs \`flutterfire configure\`, which
112
+ // generates the real file from their Firebase project. Present only so
113
+ // \`flutter analyze\` can type-check without touching the network.
114
+ // ignore_for_file: type=lint
115
+ import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
116
+
117
+ class DefaultFirebaseOptions {
118
+ static FirebaseOptions get currentPlatform => const FirebaseOptions(
119
+ apiKey: 'offline-validation-stub',
120
+ appId: 'offline-validation-stub',
121
+ messagingSenderId: 'offline-validation-stub',
122
+ projectId: 'offline-validation-stub',
123
+ );
124
+ }
125
+ `;
126
+ await fs.outputFile(
127
+ path.join(targetDir, 'lib', 'firebase_options_dev.dart'),
128
+ stub,
129
+ 'utf8',
130
+ );
131
+ }
132
+
94
133
  async function generateProject(targetDir, backend, options, hooks = {}) {
95
134
  const {
96
135
  appName,
@@ -102,6 +141,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
102
141
  includeWeb = true,
103
142
  language = 'en',
104
143
  deferGoogleAuthPatches = false,
144
+ offline = false,
105
145
  } = options;
106
146
 
107
147
  const { applyBackendSetup = null, postBuild = null } = hooks;
@@ -361,9 +401,18 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
361
401
  steps.push({ name: 'build-runner', skipped: true });
362
402
  }
363
403
 
364
- onProgress('flutterfire');
365
- const ffResult = await flutterfireConfigure(targetDir, firebaseProjectId, { includeWeb });
366
- steps.push({ name: 'flutterfire', ok: ffResult.ok, detail: ffResult.ok ? null : ffResult.error });
404
+ // Offline validation never touches the network or the live Firebase project, so
405
+ // skip flutterfire and write a compilable stub for the file it would generate.
406
+ // ffResult.ok stays false the flutterfire-dependent patches below are skipped.
407
+ let ffResult = { ok: false };
408
+ if (offline) {
409
+ await writeFirebaseOptionsDevStub(targetDir);
410
+ steps.push({ name: 'flutterfire', skipped: true, detail: 'offline (stub written)' });
411
+ } else {
412
+ onProgress('flutterfire');
413
+ ffResult = await flutterfireConfigure(targetDir, firebaseProjectId, { includeWeb });
414
+ steps.push({ name: 'flutterfire', ok: ffResult.ok, detail: ffResult.ok ? null : ffResult.error });
415
+ }
367
416
 
368
417
  // After flutterfire: patch Android, iOS and Web config files.
369
418
  if (ffResult.ok) {
@@ -421,7 +470,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
421
470
  // ONLY for FCM push — there is no Firestore there, so deploying firestore:rules
422
471
  // is incoherent (it targets a database that doesn't exist) and just hangs for
423
472
  // minutes. The data layer for those backends is Supabase/the REST API.
424
- if (backend === 'firebase' && firebaseProjectId) {
473
+ if (!offline && backend === 'firebase' && firebaseProjectId) {
425
474
  onProgress('firestore-rules');
426
475
  const rulesResult = await deployFirestoreRules(targetDir, firebaseProjectId);
427
476
  steps.push({ name: 'firestore-rules', ok: rulesResult.ok, detail: rulesResult.ok ? null : rulesResult.error });
@@ -336,6 +336,25 @@ module.exports = {
336
336
  'new.firebase.success.deployStep': '• Deploy backend (from inside the project folder):',
337
337
 
338
338
  'cli.command.deploy.description': 'Publish the server to Firebase or Supabase',
339
+ 'cli.command.releaseVersion.description': 'Announce a new app version to existing users (Firebase Remote Config)',
340
+ 'releaseVersion.intro': 'Announce version → Firebase Remote Config',
341
+ 'releaseVersion.notKasy': 'Not a Kasy project (kit_setup.json not found).',
342
+ 'releaseVersion.noFirebase': 'This command needs a Firebase project (firebase.json not found). Run it inside a project with the Firebase backend.',
343
+ 'releaseVersion.noProject': 'Could not detect the Firebase project. Pass --project <id>.',
344
+ 'releaseVersion.current': 'Installed version (pubspec): {version}',
345
+ 'releaseVersion.detectedProject': 'Firebase project: {id}',
346
+ 'releaseVersion.qVersion': 'Which version is now live in the stores?',
347
+ 'releaseVersion.qForced': 'Force this update? (blocks the app until the user updates)',
348
+ 'releaseVersion.forcedHint': 'Only force when an old version is truly broken — a wrong value locks every user out.',
349
+ 'releaseVersion.summaryOptional': 'Optional update → app_latest_version = {version}',
350
+ 'releaseVersion.summaryForced': 'FORCED update → app_min_version = {version} (+ app_latest_version)',
351
+ 'releaseVersion.qConfirm': 'Publish this to Firebase Remote Config?',
352
+ 'releaseVersion.fetching': 'Reading current Remote Config…',
353
+ 'releaseVersion.deploying': 'Publishing to Remote Config…',
354
+ 'releaseVersion.done': 'Done — {version} is live in Remote Config.',
355
+ 'releaseVersion.failed': 'Failed to update Remote Config (is the Firebase CLI installed and are you logged in? try: firebase login).',
356
+ 'releaseVersion.badVersion': 'Enter a valid SemVer like 1.4.0',
357
+ 'releaseVersion.aborted': 'Aborted.',
339
358
  'cli.command.configure.description': 'Configure optional keys (RevenueCat, Sentry, Mixpanel...) — skippable, picks up where you left off',
340
359
  'configure.title': 'App credential setup',
341
360
  'configure.notKasyProject': 'This folder is not a Kasy project (no pubspec.yaml). Cd into the project and run again.',
@@ -1134,6 +1153,10 @@ module.exports = {
1134
1153
  'update.success': 'Feature "{module}" updated successfully.',
1135
1154
  'update.componentsSuccess': 'Base components updated successfully.',
1136
1155
  'update.coreSuccess': 'Core files updated successfully.',
1156
+ 'update.mergeHint.title': 'Merge your changes with what is new',
1157
+ 'update.mergeHint.body': '{count} file(s) changed. Check with `git diff`. If you had customized any, just merge yours with the new version.',
1158
+ 'update.mergeHint.aiPrompt': 'With AI: paste into your assistant → "I ran kasy update; merge my changes from the previous commit with the new kit version in these files, keeping both."',
1159
+ 'update.mergeHint.seeAgents': 'Step by step in the project AGENTS.md.',
1137
1160
  'check.intro': 'Kasy Check — Push Notifications',
1138
1161
  'check.firebase.detected': 'Firebase backend',
1139
1162
  'check.firebase.adcInfo': 'Firebase uses Application Default Credentials — no extra setup needed.',
@@ -338,6 +338,25 @@ module.exports = {
338
338
  'new.firebase.success.deployStep': '• Desplegar backend (desde dentro de la carpeta del proyecto):',
339
339
 
340
340
  'cli.command.deploy.description': 'Publica el servidor en Firebase o Supabase',
341
+ 'cli.command.releaseVersion.description': 'Avisa a los usuarios actuales sobre una nueva versión de la app (Firebase Remote Config)',
342
+ 'releaseVersion.intro': 'Anunciar versión → Firebase Remote Config',
343
+ 'releaseVersion.notKasy': 'No es un proyecto Kasy (kit_setup.json no encontrado).',
344
+ 'releaseVersion.noFirebase': 'Este comando necesita un proyecto Firebase (firebase.json no encontrado). Ejecútalo dentro de un proyecto con backend Firebase.',
345
+ 'releaseVersion.noProject': 'No se pudo detectar el proyecto Firebase. Usa --project <id>.',
346
+ 'releaseVersion.current': 'Versión instalada (pubspec): {version}',
347
+ 'releaseVersion.detectedProject': 'Proyecto Firebase: {id}',
348
+ 'releaseVersion.qVersion': '¿Qué versión está publicada ya en las tiendas?',
349
+ 'releaseVersion.qForced': '¿Forzar esta actualización? (bloquea la app hasta que el usuario actualice)',
350
+ 'releaseVersion.forcedHint': 'Solo fuerza cuando una versión antigua esté realmente rota — un valor incorrecto bloquea a todos los usuarios.',
351
+ 'releaseVersion.summaryOptional': 'Actualización opcional → app_latest_version = {version}',
352
+ 'releaseVersion.summaryForced': 'Actualización OBLIGATORIA → app_min_version = {version} (+ app_latest_version)',
353
+ 'releaseVersion.qConfirm': '¿Publicar esto en Firebase Remote Config?',
354
+ 'releaseVersion.fetching': 'Leyendo el Remote Config actual…',
355
+ 'releaseVersion.deploying': 'Publicando en Remote Config…',
356
+ 'releaseVersion.done': 'Listo — {version} está activo en Remote Config.',
357
+ 'releaseVersion.failed': 'No se pudo actualizar Remote Config (¿está instalado el Firebase CLI y has iniciado sesión? prueba: firebase login).',
358
+ 'releaseVersion.badVersion': 'Ingresa un SemVer válido, como 1.4.0',
359
+ 'releaseVersion.aborted': 'Cancelado.',
341
360
  'cli.command.configure.description': 'Configura claves opcionales (RevenueCat, Sentry, Mixpanel...) — puede omitir, recuerda lo que falta',
342
361
  'configure.title': 'Configuración de claves del app',
343
362
  'configure.notKasyProject': 'Esta carpeta no es un proyecto Kasy (no se encontró pubspec.yaml). Entra en la carpeta del proyecto y ejecuta de nuevo.',
@@ -1132,6 +1151,10 @@ module.exports = {
1132
1151
  'update.success': 'Feature "{module}" actualizada exitosamente.',
1133
1152
  'update.componentsSuccess': 'Componentes base actualizados exitosamente.',
1134
1153
  'update.coreSuccess': 'Archivos de core actualizados exitosamente.',
1154
+ 'update.mergeHint.title': 'Combina tus cambios con lo nuevo',
1155
+ 'update.mergeHint.body': '{count} archivo(s) cambiaron. Revisa con `git diff`. Si habías personalizado alguno, solo combina el tuyo con la versión nueva.',
1156
+ 'update.mergeHint.aiPrompt': 'Con IA: pega en tu asistente → "Ejecuté kasy update; combina mis cambios del commit anterior con la versión nueva del kit en estos archivos, manteniendo ambos."',
1157
+ 'update.mergeHint.seeAgents': 'Paso a paso en el AGENTS.md del proyecto.',
1135
1158
  'check.intro': 'Kasy Check — Notificaciones Push',
1136
1159
  'check.firebase.detected': 'Backend Firebase',
1137
1160
  'check.firebase.adcInfo': 'Firebase usa Application Default Credentials — no requiere configuración extra.',
@@ -336,6 +336,25 @@ module.exports = {
336
336
  'new.firebase.success.deployStep': '• Deploy do backend (de dentro da pasta do projeto):',
337
337
 
338
338
  'cli.command.deploy.description': 'Publica o servidor no Firebase ou Supabase',
339
+ 'cli.command.releaseVersion.description': 'Avisa os usuários atuais sobre uma nova versão do app (Firebase Remote Config)',
340
+ 'releaseVersion.intro': 'Anunciar versão → Firebase Remote Config',
341
+ 'releaseVersion.notKasy': 'Não é um projeto Kasy (kit_setup.json não encontrado).',
342
+ 'releaseVersion.noFirebase': 'Este comando precisa de um projeto Firebase (firebase.json não encontrado). Rode dentro de um projeto com backend Firebase.',
343
+ 'releaseVersion.noProject': 'Não foi possível detectar o projeto Firebase. Passe --project <id>.',
344
+ 'releaseVersion.current': 'Versão instalada (pubspec): {version}',
345
+ 'releaseVersion.detectedProject': 'Projeto Firebase: {id}',
346
+ 'releaseVersion.qVersion': 'Qual versão já está publicada nas lojas?',
347
+ 'releaseVersion.qForced': 'Forçar esta atualização? (bloqueia o app até o usuário atualizar)',
348
+ 'releaseVersion.forcedHint': 'Só force quando uma versão antiga estiver realmente quebrada — um valor errado trava todos os usuários.',
349
+ 'releaseVersion.summaryOptional': 'Atualização opcional → app_latest_version = {version}',
350
+ 'releaseVersion.summaryForced': 'Atualização OBRIGATÓRIA → app_min_version = {version} (+ app_latest_version)',
351
+ 'releaseVersion.qConfirm': 'Publicar isto no Firebase Remote Config?',
352
+ 'releaseVersion.fetching': 'Lendo o Remote Config atual…',
353
+ 'releaseVersion.deploying': 'Publicando no Remote Config…',
354
+ 'releaseVersion.done': 'Pronto — {version} está no ar no Remote Config.',
355
+ 'releaseVersion.failed': 'Falha ao atualizar o Remote Config (o Firebase CLI está instalado e você fez login? tente: firebase login).',
356
+ 'releaseVersion.badVersion': 'Informe um SemVer válido, tipo 1.4.0',
357
+ 'releaseVersion.aborted': 'Cancelado.',
339
358
  'cli.command.configure.description': 'Configura chaves opcionais (RevenueCat, Sentry, Mixpanel...) — pode pular, lembra do que falta',
340
359
  'configure.title': 'Configuração de chaves do app',
341
360
  'configure.notKasyProject': 'Esta pasta não parece um projeto Kasy (não encontrei pubspec.yaml). Entre na pasta do projeto e rode novamente.',
@@ -1132,6 +1151,10 @@ module.exports = {
1132
1151
  'update.success': 'Feature "{module}" atualizada com sucesso.',
1133
1152
  'update.componentsSuccess': 'Componentes base atualizados com sucesso.',
1134
1153
  'update.coreSuccess': 'Arquivos de core atualizados com sucesso.',
1154
+ 'update.mergeHint.title': 'Juntar suas mudanças com o que veio novo',
1155
+ 'update.mergeHint.body': '{count} arquivo(s) mudaram. Veja com `git diff`. Se você tinha customizado algum, é só juntar o seu com o novo.',
1156
+ 'update.mergeHint.aiPrompt': 'Com IA: cole no seu assistente → "Rodei kasy update; junte minhas mudanças do commit anterior com a versão nova do kit nestes arquivos, mantendo as duas coisas."',
1157
+ 'update.mergeHint.seeAgents': 'Passo a passo no AGENTS.md do projeto.',
1135
1158
  'check.intro': 'Kasy Check — Notificações Push',
1136
1159
  'check.firebase.detected': 'Backend Firebase',
1137
1160
  'check.firebase.adcInfo': 'Firebase usa Application Default Credentials — nenhuma configuração extra necessária.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -31,14 +31,17 @@
31
31
  "access": "public"
32
32
  },
33
33
  "scripts": {
34
- "prepack": "node scripts/check-feature-patches.js && node scripts/bundle-template.js && node test/generated-matches-kit.test.js && node test/supabase-verify-jwt.test.js && node test/supabase-cors.test.js && node test/supabase-google-web.test.js && node test/backend-pubspec-local-reminders.test.js && node test/stripe-webhook-orphan-guard.test.js && node test/facebook-strip.test.js && node test/path-provider-pin.test.js && node test/features-flags-parity.test.js && node test/apple-web.test.js && node test/facebook.test.js",
34
+ "prepack": "node scripts/check-feature-patches.js && node scripts/check-backend-patches.js && node scripts/bundle-template.js && node test/generated-matches-kit.test.js && node test/supabase-verify-jwt.test.js && node test/supabase-cors.test.js && node test/supabase-google-web.test.js && node test/backend-pubspec-local-reminders.test.js && node test/stripe-webhook-orphan-guard.test.js && node test/facebook-strip.test.js && node test/path-provider-pin.test.js && node test/features-flags-parity.test.js && node test/apple-web.test.js && node test/facebook.test.js",
35
35
  "start": "node ./bin/kasy.js",
36
36
  "setup": "node ./bin/kasy.js setup",
37
37
  "doctor": "node ./bin/kasy.js doctor",
38
38
  "features": "node ./bin/kasy.js features",
39
39
  "validate": "node ./bin/kasy.js validate --analyze-only",
40
+ "validate:backends": "node ./scripts/bundle-template.js && node ./scripts/validate-backends.js",
40
41
  "extract:patch": "node ./scripts/extract_patch.js",
41
42
  "check:firebase": "node ./scripts/check-firebase-template.js",
43
+ "patch:check": "node ./scripts/check-backend-patches.js",
44
+ "patch:bless": "node ./scripts/check-backend-patches.js --bless",
42
45
  "test": "for f in test/*.test.js; do node \"$f\" || exit 1; done",
43
46
  "test:google-ios": "node ./test/google-ios-url-scheme.test.js",
44
47
  "test:apple-release": "node ./test/apple-release.test.js",