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