kasy-cli 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/bin/kasy.js +143 -7
  2. package/lib/commands/add.js +2 -2
  3. package/lib/commands/codemagic.js +11 -4
  4. package/lib/commands/deploy.js +3 -3
  5. package/lib/commands/favicon.js +115 -0
  6. package/lib/commands/icon.js +143 -0
  7. package/lib/commands/ios.js +20 -5
  8. package/lib/commands/new.js +8 -20
  9. package/lib/commands/remove.js +1 -1
  10. package/lib/commands/reset.js +287 -0
  11. package/lib/commands/run.js +24 -17
  12. package/lib/commands/splash.js +219 -0
  13. package/lib/commands/update.js +1 -1
  14. package/lib/scaffold/CHANGELOG.json +9 -0
  15. package/lib/scaffold/backends/api/patch/README.md +1 -1
  16. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
  17. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
  18. package/lib/scaffold/backends/api/patch/lib/main.dart +29 -10
  19. package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
  20. package/lib/scaffold/backends/firebase/tokens.js +2 -2
  21. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
  22. package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
  23. package/lib/scaffold/backends/supabase/patch/README.md +1 -1
  24. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
  25. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
  26. package/lib/scaffold/backends/supabase/patch/lib/main.dart +29 -10
  27. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
  28. package/lib/scaffold/features/README.md +15 -139
  29. package/lib/scaffold/shared/generator-utils.js +16 -15
  30. package/lib/utils/apple-release.js +85 -16
  31. package/lib/utils/checks.js +4 -105
  32. package/lib/utils/flutter-run.js +173 -0
  33. package/lib/utils/i18n.js +413 -0
  34. package/lib/utils/mobile-identity.js +35 -0
  35. package/lib/utils/ui.js +114 -0
  36. package/package.json +2 -3
  37. package/templates/firebase/README.en.md +1 -1
  38. package/templates/firebase/README.es.md +1 -1
  39. package/templates/firebase/README.md +1 -1
  40. package/templates/firebase/android/app/build.gradle.kts +10 -1
  41. package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
  42. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
  43. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +160 -11
  44. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
  45. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
  46. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
  47. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
  48. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
  49. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-night/launch_background.xml +9 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/drawable-night-v21/launch_background.xml +9 -0
  62. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  63. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  64. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  65. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  66. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  67. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  68. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  69. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  70. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  71. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  72. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  73. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  74. package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
  75. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +46 -0
  76. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  80. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  81. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +2 -1
  82. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -0
  83. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
  84. package/templates/firebase/assets/images/favicon.png +0 -0
  85. package/templates/firebase/assets/images/icon.png +0 -0
  86. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  87. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  88. package/templates/firebase/firestore.indexes.json +10 -0
  89. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
  90. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
  91. package/templates/firebase/functions/src/index.ts +1 -0
  92. package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
  93. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
  94. package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
  95. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  96. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  98. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  99. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  100. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  101. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  102. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  103. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  104. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  105. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  106. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  107. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  108. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  109. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  110. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  111. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  112. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  113. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  114. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  115. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  116. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +9 -8
  117. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  118. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +33 -0
  119. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  120. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  121. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  122. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  123. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  124. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  125. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  126. package/templates/firebase/ios/Runner/Info.plist +2 -2
  127. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
  128. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
  129. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
  130. package/templates/firebase/lib/components/kasy_button.dart +8 -0
  131. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
  132. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +67 -19
  133. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
  134. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
  135. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
  136. package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
  137. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +48 -24
  138. package/templates/firebase/lib/features/home/home_page.dart +0 -6
  139. package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
  140. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
  141. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
  142. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
  143. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
  144. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
  145. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
  146. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +4 -0
  147. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +1 -0
  148. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +13 -0
  149. package/templates/firebase/lib/features/settings/settings_page.dart +158 -18
  150. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
  151. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
  152. package/templates/firebase/lib/i18n/en.i18n.json +10 -3
  153. package/templates/firebase/lib/i18n/es.i18n.json +10 -3
  154. package/templates/firebase/lib/i18n/pt.i18n.json +10 -3
  155. package/templates/firebase/lib/main.dart +29 -10
  156. package/templates/firebase/pubspec.yaml +10 -6
  157. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +1 -1
  158. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
  159. package/templates/firebase/web/favicon.png +0 -0
  160. package/templates/firebase/web/icons/Icon-192.png +0 -0
  161. package/templates/firebase/web/icons/Icon-512.png +0 -0
  162. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  163. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  164. package/templates/firebase/web/index.html +50 -39
  165. package/templates/firebase/web/manifest.json +3 -3
  166. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  167. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  168. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  169. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  170. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  171. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  172. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  173. package/templates/firebase/web/splash/img/light-4x.png +0 -0
  174. package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +0 -124
  175. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +0 -35
  176. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +0 -35
  177. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +0 -35
  178. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +0 -12
  179. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +0 -12
  180. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +0 -12
  181. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +0 -17
  182. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +0 -17
  183. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +0 -17
  184. package/lib/scaffold/features/ci/.github/dependabot.yml +0 -16
  185. package/lib/scaffold/features/ci/.github/workflows/app.yml +0 -20
  186. package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +0 -14
  187. package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +0 -19
  188. package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +0 -163
  189. package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +0 -28
  190. package/lib/scaffold/features/ci/.gitlab-ci.yml +0 -37
  191. package/lib/scaffold/features/ci/codemagic.yaml +0 -157
  192. package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +0 -111
  193. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +0 -27
  194. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +0 -27
  195. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +0 -50
  196. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +0 -79
  197. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +0 -48
  198. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +0 -42
  199. package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +0 -147
  200. package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +0 -95
  201. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  202. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +0 -175
  203. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -76
  204. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +0 -279
  205. package/lib/scaffold/features/ios-release/.kasy/apple.env.example +0 -8
  206. package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +0 -7
  207. package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +0 -50
  208. package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +0 -50
  209. package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +0 -50
  210. package/lib/scaffold/features/ios-release/docs/ios-release.en.md +0 -41
  211. package/lib/scaffold/features/ios-release/docs/ios-release.es.md +0 -41
  212. package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +0 -41
  213. package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +0 -38
  214. package/lib/scaffold/features/ios-release/scripts/release-ios.sh +0 -137
  215. package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +0 -301
  216. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +0 -81
  217. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +0 -76
  218. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +0 -282
  219. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +0 -24
  220. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +0 -71
  221. package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +0 -92
  222. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +0 -15
  223. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +0 -78
  224. package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +0 -29
  225. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +0 -30
  226. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -66
  227. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +0 -72
  228. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +0 -92
  229. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  230. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +0 -89
  231. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +0 -94
  232. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +0 -80
  233. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +0 -139
  234. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +0 -110
  235. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +0 -84
  236. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -173
  237. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +0 -45
  238. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +0 -77
  239. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +0 -392
  240. package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +0 -116
  241. package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +0 -322
  242. package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +0 -41
  243. package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +0 -305
  244. package/templates/firebase/assets/images/app_icon.png +0 -0
  245. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  246. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  247. package/templates/firebase/assets/images/splashscreen.png +0 -0
  248. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
  249. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
@@ -15,22 +15,10 @@ const path = require('node:path');
15
15
  const crypto = require('node:crypto');
16
16
  const kleur = require('kleur');
17
17
 
18
- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
19
-
20
18
  function generateWebhookKey() {
21
19
  return 'rc_wh_' + crypto.randomBytes(16).toString('hex');
22
20
  }
23
21
 
24
-
25
-
26
- async function waitWithCountdown(seconds, label) {
27
- for (let i = seconds; i > 0; i--) {
28
- process.stdout.write(`\r ${label} ${i}s… `);
29
- await sleep(1000);
30
- }
31
- process.stdout.write(`\r ${label} pronto! \n`);
32
- }
33
-
34
22
  function openUrl(url) {
35
23
  try {
36
24
  const { exec } = require('node:child_process');
@@ -716,7 +704,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
716
704
  const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
717
705
  let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
718
706
  ui.log.info(`${tr('new.firebase.create.estimatedTime')}\n${tr('new.internet.warning')}`);
719
- const ps1 = ui.makeStepper();
707
+ const ps1 = ui.makeTimedStepper();
720
708
  ps1.next(tr('new.firebase.create.creating'));
721
709
  const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
722
710
  includeWeb: firebaseIncludeWeb,
@@ -796,7 +784,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
796
784
  }
797
785
  ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
798
786
  selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
799
- const ps2 = ui.makeStepper();
787
+ const ps2 = ui.makeTimedStepper();
800
788
  ps2.next(tr('new.firebase.create.creating'));
801
789
  lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
802
790
  includeWeb: firebaseIncludeWeb,
@@ -889,7 +877,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
889
877
  const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
890
878
  const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
891
879
  ui.log.info(tr('new.internet.warning'));
892
- const ps3 = ui.makeStepper();
880
+ const ps3 = ui.makeTimedStepper();
893
881
  ps3.next(tr('new.firebase.create.creatingPush'));
894
882
  const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
895
883
  includeWeb: true,
@@ -990,7 +978,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
990
978
  });
991
979
  supabaseDbPassword = dbPassword;
992
980
  ui.log.info(tr('new.internet.warning'));
993
- const createSpinner = ui.spinner();
981
+ const createSpinner = ui.timedSpinner();
994
982
  createSpinner.start(tr('new.supabase.creating'));
995
983
  supabaseCreateResult = await createProjectAndGetKeys(
996
984
  core.appName.trim().replace(/\s+/g, '-').toLowerCase(),
@@ -1087,7 +1075,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1087
1075
 
1088
1076
  // ── Firebase existing project: enable APIs + create Firestore/Storage ───
1089
1077
  if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
1090
- const ps4 = ui.makeStepper();
1078
+ const ps4 = ui.makeTimedStepper();
1091
1079
  ps4.next(stepProgress('enable-apis', language));
1092
1080
  const existingSetup = await setupExistingProject(core.firebaseProjectId, {
1093
1081
  onProgress: (key, data) => {
@@ -1388,7 +1376,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1388
1376
  // Stepper shows each step closing as ✦ and the next starting as ⠙ — so the
1389
1377
  // user gets explicit "X done → Y starting" feedback instead of a single
1390
1378
  // spinner with a mutating message.
1391
- const stepper = ui.makeStepper();
1379
+ const stepper = ui.makeTimedStepper();
1392
1380
  // First step started here so even silent prep work shows progress.
1393
1381
  stepper.next(stepProgress('project-setup', language));
1394
1382
 
@@ -1512,7 +1500,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1512
1500
  // ── FCM Service Account key (best effort via gcloud — Firebase uses ADC, Supabase needs JSON) ──
1513
1501
  let fcmServiceAccountJson = null;
1514
1502
  if (answers.firebaseProjectId) {
1515
- const fcmSpinner = ui.spinner();
1503
+ const fcmSpinner = ui.timedSpinner();
1516
1504
  fcmSpinner.start(tr('new.fcm.generating'));
1517
1505
  const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
1518
1506
  fcmSpinner.stop(tr('new.fcm.generating'));
@@ -1562,7 +1550,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1562
1550
 
1563
1551
  // ── API: FCM Service Account key — save to .kasy/ for server configuration ──
1564
1552
  if (backend === 'api' && answers.firebaseProjectId) {
1565
- const fcmSpinner = ui.spinner();
1553
+ const fcmSpinner = ui.timedSpinner();
1566
1554
  fcmSpinner.start(tr('new.fcm.generating'));
1567
1555
  const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
1568
1556
  fcmSpinner.stop(tr('new.fcm.generating'));
@@ -444,7 +444,7 @@ async function runRemove(module, options = {}) {
444
444
  'revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback',
445
445
  ].includes(normalized);
446
446
  if (needsBuildRunner) {
447
- const spinner = ui.spinner();
447
+ const spinner = ui.timedSpinner();
448
448
  spinner.start(t('remove.buildRunner'));
449
449
  try {
450
450
  await execAsync(
@@ -0,0 +1,287 @@
1
+ const path = require('node:path');
2
+ const { spawnSync } = require('node:child_process');
3
+ const fs = require('fs-extra');
4
+ const kleur = require('kleur');
5
+ const ui = require('../utils/ui');
6
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
7
+ const { printCompactHeader } = require('../utils/brand');
8
+ const { readBundleId, readPackageName } = require('../utils/mobile-identity');
9
+ const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
10
+
11
+ function runCmd(cmd, args) {
12
+ const res = spawnSync(cmd, args, { encoding: 'utf8' });
13
+ return {
14
+ code: res.status,
15
+ stdout: (res.stdout || '').trim(),
16
+ stderr: (res.stderr || '').trim(),
17
+ };
18
+ }
19
+
20
+ async function listFlutterDevices(projectDir) {
21
+ const res = spawnSync('flutter', ['devices', '--machine'], {
22
+ cwd: projectDir,
23
+ encoding: 'utf8',
24
+ });
25
+ if (res.status !== 0) return [];
26
+ try {
27
+ const parsed = JSON.parse(res.stdout);
28
+ return Array.isArray(parsed) ? parsed : [];
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function classifyTarget(device) {
35
+ const platform = (device.targetPlatform || '').toLowerCase();
36
+ if (platform === 'ios') {
37
+ return device.emulator ? 'ios-simulator' : 'ios-device';
38
+ }
39
+ if (platform.startsWith('android')) {
40
+ return device.emulator ? 'android-emulator' : 'android-device';
41
+ }
42
+ if (platform.startsWith('web')) {
43
+ return 'web';
44
+ }
45
+ return 'unknown';
46
+ }
47
+
48
+ function filterDevices(devices, options) {
49
+ return devices.filter((d) => {
50
+ const kind = classifyTarget(d);
51
+ if (options.device && d.id === options.device) return true;
52
+ if (options.ios && (kind === 'ios-simulator' || kind === 'ios-device')) {
53
+ return true;
54
+ }
55
+ if (
56
+ options.android &&
57
+ (kind === 'android-emulator' || kind === 'android-device')
58
+ ) {
59
+ return true;
60
+ }
61
+ if (options.web && kind === 'web') return true;
62
+ if (!options.ios && !options.android && !options.web && !options.device) {
63
+ return true;
64
+ }
65
+ return false;
66
+ });
67
+ }
68
+
69
+ async function pickDevice(devices, t) {
70
+ if (devices.length === 0) return null;
71
+ if (devices.length === 1) return devices[0];
72
+ const choice = await ui.select({
73
+ message: t('reset.prompt.pickDevice'),
74
+ options: devices.map((d) => ({
75
+ value: d.id,
76
+ label: `${d.name} ${kleur.dim(`(${classifyTarget(d)})`)}`,
77
+ })),
78
+ });
79
+ return devices.find((d) => d.id === choice) || null;
80
+ }
81
+
82
+ function resetIosSimulator(device, bundleId, t) {
83
+ ui.log.message(kleur.dim(`xcrun simctl uninstall ${device.id} ${bundleId}`));
84
+ const res = runCmd('xcrun', ['simctl', 'uninstall', device.id, bundleId]);
85
+ if (res.code !== 0) {
86
+ // simctl returns 0 even when app wasn't installed; non-zero is a real error
87
+ ui.log.warn(res.stderr || t('reset.warn.iosUninstallFailed'));
88
+ return false;
89
+ }
90
+ ui.log.success(t('reset.success.uninstalled'));
91
+ return true;
92
+ }
93
+
94
+ function resetIosDevice(device, bundleId, t) {
95
+ // Try Apple's devicectl (ships with Xcode 15+). Falls back to a manual
96
+ // instruction when devicectl isn't available or the uninstall fails.
97
+ const probe = runCmd('xcrun', ['devicectl', '--version']);
98
+ if (probe.code !== 0) {
99
+ noticeIosPhysical(t);
100
+ return false;
101
+ }
102
+ ui.log.message(
103
+ kleur.dim(
104
+ `xcrun devicectl device uninstall app --device ${device.id} ${bundleId}`
105
+ )
106
+ );
107
+ const res = runCmd('xcrun', [
108
+ 'devicectl',
109
+ 'device',
110
+ 'uninstall',
111
+ 'app',
112
+ '--device',
113
+ device.id,
114
+ bundleId,
115
+ ]);
116
+ if (res.code !== 0) {
117
+ ui.log.warn(res.stderr || t('reset.warn.iosDeviceUninstallFailed'));
118
+ noticeIosPhysical(t);
119
+ return false;
120
+ }
121
+ ui.log.success(t('reset.success.uninstalled'));
122
+ return true;
123
+ }
124
+
125
+ function resetAndroid(device, packageName, t) {
126
+ ui.log.message(kleur.dim(`adb -s ${device.id} uninstall ${packageName}`));
127
+ const res = runCmd('adb', ['-s', device.id, 'uninstall', packageName]);
128
+ // adb prints "Success" or "Failure [DELETE_FAILED_INTERNAL_ERROR]" / "not installed"
129
+ const out = `${res.stdout}\n${res.stderr}`.toLowerCase();
130
+ if (out.includes('success')) {
131
+ ui.log.success(t('reset.success.uninstalled'));
132
+ return true;
133
+ }
134
+ if (out.includes('not installed')) {
135
+ ui.log.message(kleur.dim(`– ${t('reset.info.notInstalled')}`));
136
+ return true;
137
+ }
138
+ ui.log.warn(res.stdout || res.stderr || t('reset.warn.androidUninstallFailed'));
139
+ return false;
140
+ }
141
+
142
+ function noticeIosPhysical(t) {
143
+ ui.log.warn(t('reset.warn.iosDeviceManual'));
144
+ }
145
+
146
+ function isXcodeRunning() {
147
+ const res = spawnSync('pgrep', ['-x', 'Xcode'], { encoding: 'utf8' });
148
+ return res.status === 0 && (res.stdout || '').trim().length > 0;
149
+ }
150
+
151
+ async function ensureXcodeClosed(t) {
152
+ if (!isXcodeRunning()) return true;
153
+ const choice = await ui.select({
154
+ message: t('reset.prompt.xcodeOpen'),
155
+ options: [
156
+ { value: 'close', label: t('reset.prompt.xcodeOpen.close') },
157
+ { value: 'skip', label: t('reset.prompt.xcodeOpen.skip') },
158
+ { value: 'cancel', label: t('reset.prompt.xcodeOpen.cancel') },
159
+ ],
160
+ initialValue: 'close',
161
+ });
162
+ if (choice === 'cancel') return false;
163
+ if (choice === 'close') {
164
+ spawnSync('osascript', ['-e', 'tell application "Xcode" to quit'], {
165
+ encoding: 'utf8',
166
+ });
167
+ // Give Xcode a moment to release the debug session before we proceed.
168
+ await new Promise((resolve) => setTimeout(resolve, 1500));
169
+ ui.log.message(kleur.dim(t('reset.info.xcodeClosed')));
170
+ }
171
+ return true;
172
+ }
173
+
174
+ function noticeWeb(t) {
175
+ ui.log.warn(t('reset.warn.webIncognito'));
176
+ }
177
+
178
+ function runFlutterOnDevice(device, projectDir, t) {
179
+ return spawnFlutterWithSpinner(['run', '-d', device.id], projectDir, t);
180
+ }
181
+
182
+ async function runReset(directory, options = {}) {
183
+ const t = createTranslator(options.language || detectDefaultLanguage());
184
+ const projectDir = path.resolve(directory || '.');
185
+
186
+ if (!(await fs.pathExists(path.join(projectDir, 'pubspec.yaml')))) {
187
+ throw new Error(t('reset.error.notFlutterProject'));
188
+ }
189
+
190
+ printCompactHeader(t);
191
+ ui.intro(t('reset.title'));
192
+
193
+ const [bundleId, packageName] = await Promise.all([
194
+ readBundleId(projectDir),
195
+ readPackageName(projectDir),
196
+ ]);
197
+ if (!bundleId && !packageName) {
198
+ throw new Error(t('reset.error.noIdentifier'));
199
+ }
200
+
201
+ ui.log.message(`${kleur.dim('iOS bundle id:')} ${bundleId || kleur.dim('—')}`);
202
+ ui.log.message(`${kleur.dim('Android package:')} ${packageName || kleur.dim('—')}`);
203
+
204
+ const scanSpinner = ui.spinner();
205
+ scanSpinner.start(t('reset.scanning'));
206
+ const allDevices = await listFlutterDevices(projectDir);
207
+ const devices = filterDevices(allDevices, options);
208
+ scanSpinner.stop(t('reset.scanning'));
209
+
210
+ if (devices.length === 0) {
211
+ ui.log.warn(t('reset.warn.noDevices'));
212
+ return;
213
+ }
214
+
215
+ const target = await pickDevice(devices, t);
216
+ if (!target) {
217
+ ui.log.warn(t('reset.warn.nothingSelected'));
218
+ return;
219
+ }
220
+
221
+ const kind = classifyTarget(target);
222
+
223
+ if (kind === 'ios-device') {
224
+ const proceed = await ensureXcodeClosed(t);
225
+ if (!proceed) {
226
+ ui.outro(t('reset.outro.cancelled'));
227
+ return;
228
+ }
229
+ }
230
+
231
+ ui.log.step(kleur.bold(`${t('reset.resetting')}: ${target.name}`));
232
+
233
+ let didReset = false;
234
+ switch (kind) {
235
+ case 'ios-simulator':
236
+ if (!bundleId) {
237
+ ui.log.error(t('reset.error.noBundleId'));
238
+ break;
239
+ }
240
+ didReset = resetIosSimulator(target, bundleId, t);
241
+ break;
242
+ case 'ios-device':
243
+ if (!bundleId) {
244
+ ui.log.error(t('reset.error.noBundleId'));
245
+ break;
246
+ }
247
+ didReset = resetIosDevice(target, bundleId, t);
248
+ break;
249
+ case 'android-emulator':
250
+ case 'android-device':
251
+ if (!packageName) {
252
+ ui.log.error(t('reset.error.noPackageName'));
253
+ break;
254
+ }
255
+ didReset = resetAndroid(target, packageName, t);
256
+ break;
257
+ case 'web':
258
+ noticeWeb(t);
259
+ break;
260
+ default:
261
+ ui.log.warn(`${t('reset.warn.unknownPlatform')}: ${kind}`);
262
+ }
263
+
264
+ if (!didReset) {
265
+ ui.outro(t('reset.outro.skipped'));
266
+ return;
267
+ }
268
+
269
+ if (options.reinstall === false) {
270
+ ui.outro(t('reset.outro.uninstalledOnly'));
271
+ return;
272
+ }
273
+
274
+ const proceed = await ui.confirm({
275
+ message: t('reset.prompt.reinstallNow'),
276
+ initialValue: true,
277
+ });
278
+ if (!proceed) {
279
+ ui.outro(t('reset.outro.uninstalledOnly'));
280
+ return;
281
+ }
282
+
283
+ ui.log.step(kleur.bold(t('reset.reinstalling')));
284
+ await runFlutterOnDevice(target, projectDir, t);
285
+ }
286
+
287
+ module.exports = { runReset };
@@ -1,9 +1,9 @@
1
- const { spawn } = require('node:child_process');
2
1
  const path = require('node:path');
3
2
  const fs = require('fs-extra');
4
3
  const kleur = require('kleur');
5
4
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
6
5
  const { printCompactHeader } = require('../utils/brand');
6
+ const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
7
7
 
8
8
  /**
9
9
  * Read dart-define args from .vscode/launch.json.
@@ -54,25 +54,32 @@ async function runRun(directory, options = {}) {
54
54
 
55
55
  const args = ['run', ...deviceArgs, ...dartDefines];
56
56
 
57
+ const envDefine = dartDefines.find((a) => a.startsWith('--dart-define=ENV='));
58
+ const envValue = envDefine ? envDefine.split('=').pop() : null;
59
+ const deviceLabel = options.web
60
+ ? 'chrome'
61
+ : options.ios
62
+ ? 'ios'
63
+ : options.android
64
+ ? 'android'
65
+ : options.device || null;
66
+ const summaryParts = [];
67
+ if (envValue) summaryParts.push(`ENV=${envValue}`);
68
+ if (deviceLabel) summaryParts.push(`device: ${deviceLabel}`);
69
+ const summary = summaryParts.length ? ` (${summaryParts.join(', ')})` : '';
70
+
57
71
  printCompactHeader(t);
58
- console.log(kleur.bold(`${t('run.launching')}`));
59
- console.log(kleur.dim(` flutter ${args.join(' ')}`));
72
+ console.log(kleur.bold(`${t('run.launching')}${summary}`));
60
73
  console.log(kleur.dim(` ✦ ${t('run.updateHint.prefix')} `) + kleur.cyan('kasy update') + kleur.dim(` ${t('run.updateHint.suffix')}\n`));
61
74
 
62
- return new Promise((resolve, reject) => {
63
- const proc = spawn('flutter', args, { cwd: projectDir, stdio: 'inherit' });
64
- proc.on('close', (code) => {
65
- if (code === 0) resolve();
66
- else reject(new Error(`flutter run exited with code ${code}`));
67
- });
68
- proc.on('error', (err) => {
69
- if (err.code === 'ENOENT') {
70
- reject(new Error(t('run.error.flutterNotFound')));
71
- } else {
72
- reject(err);
73
- }
74
- });
75
- });
75
+ try {
76
+ await spawnFlutterWithSpinner(args, projectDir, t);
77
+ } catch (err) {
78
+ if (err.code === 'ENOENT') {
79
+ throw new Error(t('run.error.flutterNotFound'));
80
+ }
81
+ throw err;
82
+ }
76
83
  }
77
84
 
78
85
  module.exports = { runRun };
@@ -0,0 +1,219 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const fsp = require('node:fs/promises');
4
+ const { exec } = require('node:child_process');
5
+ const { promisify } = require('node:util');
6
+ const kleur = require('kleur');
7
+ const ui = require('../utils/ui');
8
+ const { printCompactHeader } = require('../utils/brand');
9
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ const ASSETS_DIR = path.join('assets', 'images');
14
+ const LIGHT_NAME = 'splash_logo_light.png';
15
+ const DARK_NAME = 'splash_logo_dark.png';
16
+
17
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
18
+
19
+ /**
20
+ * Parse the PNG IHDR chunk to read color type and detect transparency.
21
+ *
22
+ * PNG layout: 8-byte signature + chunks. The IHDR chunk is always first,
23
+ * placed at byte offset 8. It holds: length(4) + "IHDR"(4) + data(13) + crc(4).
24
+ * Color type is the 10th byte of data → absolute offset 25.
25
+ *
26
+ * Color types with alpha or transparency support:
27
+ * 4 = grayscale + alpha
28
+ * 6 = truecolor + alpha
29
+ * 3 = indexed (transparency only if a tRNS chunk is present)
30
+ *
31
+ * @param {string} filePath
32
+ * @returns {Promise<{ valid: boolean, hasAlpha: boolean, colorType: number, width: number, height: number }>}
33
+ */
34
+ async function inspectPng(filePath) {
35
+ const handle = await fsp.open(filePath, 'r');
36
+ try {
37
+ const header = Buffer.alloc(33);
38
+ await handle.read(header, 0, 33, 0);
39
+ const sig = header.subarray(0, 8);
40
+ if (!sig.equals(PNG_SIGNATURE)) {
41
+ return { valid: false, hasAlpha: false, colorType: -1, width: 0, height: 0 };
42
+ }
43
+ const width = header.readUInt32BE(16);
44
+ const height = header.readUInt32BE(20);
45
+ const colorType = header.readUInt8(25);
46
+
47
+ let hasAlpha = colorType === 4 || colorType === 6;
48
+
49
+ if (!hasAlpha && colorType === 3) {
50
+ const { size } = await handle.stat();
51
+ const remaining = Math.min(size - 33, 256 * 1024);
52
+ if (remaining > 0) {
53
+ const tail = Buffer.alloc(remaining);
54
+ await handle.read(tail, 0, remaining, 33);
55
+ if (tail.includes(Buffer.from('tRNS'))) {
56
+ hasAlpha = true;
57
+ }
58
+ }
59
+ }
60
+
61
+ return { valid: true, hasAlpha, colorType, width, height };
62
+ } finally {
63
+ await handle.close();
64
+ }
65
+ }
66
+
67
+ async function assertKasyProject(projectDir, t) {
68
+ const kitSetupPath = path.join(projectDir, 'kit_setup.json');
69
+ const pubspecPath = path.join(projectDir, 'pubspec.yaml');
70
+ if (!(await fs.pathExists(kitSetupPath)) && !(await fs.pathExists(pubspecPath))) {
71
+ throw new Error(t('splash.error.notKasyProject'));
72
+ }
73
+ const assetsDir = path.join(projectDir, ASSETS_DIR);
74
+ await fs.ensureDir(assetsDir);
75
+ }
76
+
77
+ /**
78
+ * @param {string} flagValue
79
+ */
80
+ function resolveInputPath(flagValue) {
81
+ if (!flagValue) return null;
82
+ const expanded = flagValue.startsWith('~')
83
+ ? path.join(require('node:os').homedir(), flagValue.slice(1))
84
+ : flagValue;
85
+ return path.resolve(expanded);
86
+ }
87
+
88
+ /**
89
+ * @param {string} projectDir
90
+ * @param {{ light?: string, dark?: string, skipGenerate?: boolean, language?: string }} options
91
+ */
92
+ async function runSplash(projectDir, options = {}) {
93
+ const language = options.language || detectDefaultLanguage();
94
+ const t = createTranslator(language);
95
+
96
+ printCompactHeader();
97
+ ui.intro(kleur.bold().cyan(t('splash.intro')));
98
+
99
+ await assertKasyProject(projectDir, t);
100
+
101
+ const lightPath = resolveInputPath(options.light);
102
+ const darkPath = resolveInputPath(options.dark);
103
+
104
+ if (!lightPath || !darkPath) {
105
+ ui.log.error(t('splash.error.bothRequired'));
106
+ ui.log.message(kleur.dim('kasy splash --light <light.png> --dark <dark.png>'));
107
+ process.exit(1);
108
+ }
109
+
110
+ for (const [role, p] of [['light', lightPath], ['dark', darkPath]]) {
111
+ if (!(await fs.pathExists(p))) {
112
+ ui.log.error(t('splash.error.fileNotFound', { path: p }));
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ const inspectSpinner = ui.spinner();
118
+ inspectSpinner.start(t('splash.validating'));
119
+
120
+ let lightInfo;
121
+ let darkInfo;
122
+ try {
123
+ lightInfo = await inspectPng(lightPath);
124
+ darkInfo = await inspectPng(darkPath);
125
+ } catch (err) {
126
+ inspectSpinner.stop(`✖ ${err.message || t('splash.error.notPng')}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ if (!lightInfo.valid || !darkInfo.valid) {
131
+ inspectSpinner.stop(`✖ ${t('splash.error.notPng')}`);
132
+ process.exit(1);
133
+ }
134
+
135
+ inspectSpinner.stop(t('splash.validated'));
136
+
137
+ const warnings = [];
138
+ if (!lightInfo.hasAlpha) {
139
+ warnings.push(t('splash.warn.noAlphaLight', { path: path.basename(lightPath) }));
140
+ }
141
+ if (!darkInfo.hasAlpha) {
142
+ warnings.push(t('splash.warn.noAlphaDark', { path: path.basename(darkPath) }));
143
+ }
144
+ if (lightInfo.width < 768 || lightInfo.height < 768) {
145
+ warnings.push(t('splash.warn.smallLight', {
146
+ w: lightInfo.width,
147
+ h: lightInfo.height,
148
+ }));
149
+ }
150
+ if (darkInfo.width < 768 || darkInfo.height < 768) {
151
+ warnings.push(t('splash.warn.smallDark', {
152
+ w: darkInfo.width,
153
+ h: darkInfo.height,
154
+ }));
155
+ }
156
+ if (warnings.length > 0) {
157
+ ui.note(warnings.map((w) => `${kleur.yellow('⚠')} ${w}`).join('\n'), t('splash.warn.title'));
158
+ }
159
+
160
+ const destLight = path.join(projectDir, ASSETS_DIR, LIGHT_NAME);
161
+ const destDark = path.join(projectDir, ASSETS_DIR, DARK_NAME);
162
+
163
+ const copySpinner = ui.spinner();
164
+ copySpinner.start(t('splash.copying'));
165
+ await fs.copy(lightPath, destLight, { overwrite: true });
166
+ await fs.copy(darkPath, destDark, { overwrite: true });
167
+ copySpinner.stop(t('splash.copied'));
168
+
169
+ if (options.skipGenerate) {
170
+ ui.note(t('splash.skipGenerate.hint'), t('splash.skipGenerate.title'));
171
+ ui.outro(t('splash.done'));
172
+ return;
173
+ }
174
+
175
+ const genSpinner = ui.spinner();
176
+ genSpinner.start(t('splash.generating'));
177
+ const result = await runFlutterNativeSplash(projectDir);
178
+ if (result.ok) {
179
+ genSpinner.stop(t('splash.generated'));
180
+ } else {
181
+ genSpinner.stop(`⚠ ${t('splash.error.generateFailed')}`);
182
+ if (result.stderr) {
183
+ ui.log.message(kleur.dim(result.stderr.split('\n').slice(0, 8).join('\n')));
184
+ }
185
+ ui.log.message(kleur.dim('dart run flutter_native_splash:create'));
186
+ process.exit(1);
187
+ }
188
+
189
+ const summary = [
190
+ `${kleur.bold(t('splash.summary.light'))}: ${kleur.white(LIGHT_NAME)} (${lightInfo.width}x${lightInfo.height})`,
191
+ `${kleur.bold(t('splash.summary.dark'))}: ${kleur.white(DARK_NAME)} (${darkInfo.width}x${darkInfo.height})`,
192
+ ].join('\n');
193
+ ui.note(summary, t('splash.summary.title'));
194
+
195
+ ui.outro(t('splash.done'));
196
+ }
197
+
198
+ async function runFlutterNativeSplash(projectDir) {
199
+ try {
200
+ const { stdout, stderr } = await execAsync(
201
+ 'dart run flutter_native_splash:create',
202
+ {
203
+ cwd: projectDir,
204
+ maxBuffer: 16 * 1024 * 1024,
205
+ timeout: 240_000,
206
+ },
207
+ );
208
+ return { ok: true, stdout, stderr };
209
+ } catch (err) {
210
+ return {
211
+ ok: false,
212
+ error: err.message,
213
+ stdout: err.stdout || '',
214
+ stderr: err.stderr || '',
215
+ };
216
+ }
217
+ }
218
+
219
+ module.exports = { runSplash, inspectPng };
@@ -374,7 +374,7 @@ async function runUpdate(module, options = {}) {
374
374
 
375
375
  // build_runner (only for modules that generate code)
376
376
  if (NEEDS_BUILD_RUNNER.includes(normalized)) {
377
- const spinner = ui.spinner();
377
+ const spinner = ui.timedSpinner();
378
378
  spinner.start(t('update.buildRunner'));
379
379
  try {
380
380
  await execAsync(
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.13.0": {
3
+ "modules": {
4
+ "onboarding": {
5
+ "pt": "Botão \"Já tem conta? Entrar\" na primeira tela do onboarding — quem já tem conta entra direto sem passar pelo fluxo todo, sem precisar criar usuário anônimo antes",
6
+ "en": "\"Already have an account? Log in\" button on the first onboarding screen — returning users go straight to sign-in instead of going through the full flow as an anonymous user first",
7
+ "es": "Botón \"¿Ya tienes cuenta? Iniciar sesión\" en la primera pantalla del onboarding — quien ya tiene cuenta entra directo sin pasar por todo el flujo como usuario anónimo"
8
+ }
9
+ }
10
+ },
2
11
  "1.10.0": {
3
12
  "modules": {
4
13
  "widget": {