kasy-cli 1.21.8 → 1.22.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 (269) hide show
  1. package/lib/commands/add.js +93 -80
  2. package/lib/commands/configure.js +100 -32
  3. package/lib/commands/doctor.js +28 -2
  4. package/lib/commands/new.js +86 -38
  5. package/lib/commands/notifications.js +1 -1
  6. package/lib/commands/remove.js +43 -15
  7. package/lib/commands/run.js +2 -2
  8. package/lib/commands/update.js +2 -2
  9. package/lib/scaffold/CHANGELOG.json +14 -0
  10. package/lib/scaffold/backends/api/generator.js +14 -14
  11. package/lib/scaffold/backends/api/patch/README.md +83 -0
  12. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
  14. package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
  15. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
  16. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
  18. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
  19. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
  20. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
  21. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
  22. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  23. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  24. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
  25. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  26. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
  27. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  28. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
  29. package/lib/scaffold/backends/firebase/deploy.js +87 -13
  30. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +14 -6
  31. package/lib/scaffold/backends/firebase/generator.js +5 -5
  32. package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
  33. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  34. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  35. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  37. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  40. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  41. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  42. package/lib/scaffold/backends/supabase/generator.js +17 -17
  43. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  44. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  45. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  46. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  48. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  49. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  50. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  51. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  52. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  53. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  55. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  56. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  57. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  59. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  60. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  61. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  62. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  63. package/lib/scaffold/catalog.js +9 -11
  64. package/lib/scaffold/generate.js +45 -31
  65. package/lib/scaffold/shared/generator-utils.js +188 -81
  66. package/lib/scaffold/shared/sort-imports.js +191 -0
  67. package/lib/scaffold/shared/template-strings.js +3 -3
  68. package/lib/utils/checks.js +2 -2
  69. package/lib/utils/i18n/messages-en.js +50 -35
  70. package/lib/utils/i18n/messages-es.js +50 -35
  71. package/lib/utils/i18n/messages-pt.js +52 -37
  72. package/lib/utils/updates.js +15 -15
  73. package/package.json +1 -1
  74. package/templates/firebase/.env.example +2 -2
  75. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  80. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  81. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  82. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  83. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  84. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  85. package/templates/firebase/firestore.rules +24 -5
  86. package/templates/firebase/functions/package-lock.json +22 -1
  87. package/templates/firebase/functions/package.json +2 -1
  88. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  89. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  90. package/templates/firebase/functions/src/index.ts +8 -2
  91. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  92. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  93. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  94. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  95. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  96. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  98. package/templates/firebase/lib/components/components.dart +4 -1
  99. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  100. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  101. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  102. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  103. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  104. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
  105. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  106. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  108. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  109. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  110. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  111. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  112. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
  113. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  114. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  115. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
  116. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  117. package/templates/firebase/lib/core/config/features.dart +5 -4
  118. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  119. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  120. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  121. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  122. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  123. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  124. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  125. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  126. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  127. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  128. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  129. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  130. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  131. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  132. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  133. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  134. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  135. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  136. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  137. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  138. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  139. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  140. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  141. package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
  142. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  143. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  144. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  145. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  146. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  147. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  148. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  149. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  150. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  151. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  152. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  153. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  154. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  155. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  156. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  157. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  158. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  159. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  160. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  161. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
  162. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  163. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  164. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  165. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  166. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  167. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  168. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  169. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  170. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  171. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  172. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  173. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  174. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  175. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  176. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  177. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  178. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  182. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  183. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  184. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  192. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  193. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  194. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  198. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  199. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  200. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  201. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  202. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  203. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  204. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  205. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  206. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  208. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  209. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  210. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  211. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  212. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  213. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  214. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  215. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  216. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  226. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  227. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  228. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  229. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  230. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  231. package/templates/firebase/lib/main.dart +6 -3
  232. package/templates/firebase/lib/router.dart +15 -23
  233. package/templates/firebase/pubspec.yaml +4 -5
  234. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  235. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  236. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  238. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  239. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  240. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  241. package/templates/firebase/test/test_utils.dart +6 -6
  242. package/templates/firebase/web/index.html +5 -2
  243. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  244. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  245. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  246. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  247. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  249. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  250. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  251. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  252. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  253. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  254. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  255. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  256. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  257. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  258. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  268. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  269. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -12,6 +12,7 @@ import 'package:kasy_kit/features/notifications/api/notifications_api.dart';
12
12
  import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifier.dart';
13
13
  import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
14
14
  import 'package:kasy_kit/i18n/translations.g.dart';
15
+ import 'package:package_info_plus/package_info_plus.dart';
15
16
 
16
17
  class SendPushNotificationPage extends ConsumerStatefulWidget {
17
18
  const SendPushNotificationPage({super.key});
@@ -38,6 +39,15 @@ class _SendPushNotificationPageState
38
39
 
39
40
  bool _sendToAll = false;
40
41
  final List<String> _emails = [];
42
+ String _appName = '';
43
+
44
+ @override
45
+ void initState() {
46
+ super.initState();
47
+ PackageInfo.fromPlatform().then((info) {
48
+ if (mounted) setState(() => _appName = info.appName);
49
+ });
50
+ }
41
51
 
42
52
  @override
43
53
  void dispose() {
@@ -165,6 +175,28 @@ class _SendPushNotificationPageState
165
175
  child: Column(
166
176
  crossAxisAlignment: CrossAxisAlignment.stretch,
167
177
  children: [
178
+ Padding(
179
+ padding: const EdgeInsets.only(
180
+ left: KasySpacing.xs,
181
+ bottom: KasySpacing.sm,
182
+ ),
183
+ child: Text(
184
+ tr.send_push_preview_label.toUpperCase(),
185
+ style: context.textTheme.labelMedium?.copyWith(
186
+ color: context.colors.muted,
187
+ letterSpacing: 1.2,
188
+ fontWeight: FontWeight.w700,
189
+ fontSize: 12,
190
+ ),
191
+ ),
192
+ ),
193
+ _NotificationPreview(
194
+ appName: _appName,
195
+ title: _titleCtrl.text,
196
+ body: _bodyCtrl.text,
197
+ imageUrl: _imageCtrl.text,
198
+ ),
199
+ const SizedBox(height: KasySpacing.lg),
168
200
  SettingsSwitchTile(
169
201
  icon: KasyIcons.notificationActive,
170
202
  title: tr.send_push_to_all,
@@ -214,6 +246,7 @@ class _SendPushNotificationPageState
214
246
  hint: tr.send_push_image_hint,
215
247
  controller: _imageCtrl,
216
248
  focusNode: _imageFocus,
249
+ onChanged: (_) => setState(() {}),
217
250
  textInputAction: TextInputAction.next,
218
251
  onEditingComplete: () => _routeFocus.requestFocus(),
219
252
  ),
@@ -364,3 +397,206 @@ class _EmailChip extends StatelessWidget {
364
397
  );
365
398
  }
366
399
  }
400
+
401
+ // ─────────────────────────────────────────────────────────────────────────────
402
+ // Live notification preview — an iOS-style banner that mirrors what the push
403
+ // will look like, updating as the admin types. Cosmetic only (no logic).
404
+ // ─────────────────────────────────────────────────────────────────────────────
405
+
406
+ class _NotificationPreview extends StatelessWidget {
407
+ final String appName;
408
+ final String title;
409
+ final String body;
410
+ final String imageUrl;
411
+
412
+ const _NotificationPreview({
413
+ required this.appName,
414
+ required this.title,
415
+ required this.body,
416
+ required this.imageUrl,
417
+ });
418
+
419
+ @override
420
+ Widget build(BuildContext context) {
421
+ final tr = t.settings.admin;
422
+ final String name = appName.trim().isEmpty ? 'App' : appName.trim();
423
+ final String url = imageUrl.trim();
424
+ final bool showImage = url.startsWith('http');
425
+ final bool titleEmpty = title.trim().isEmpty;
426
+ final bool bodyEmpty = body.trim().isEmpty;
427
+
428
+ return Container(
429
+ padding: const EdgeInsets.all(12),
430
+ decoration: BoxDecoration(
431
+ color: context.colors.surface,
432
+ borderRadius: BorderRadius.circular(20),
433
+ border: Border.all(
434
+ color: context.colors.outline.withValues(
435
+ alpha: context.isDark ? 0.4 : 0.2,
436
+ ),
437
+ ),
438
+ boxShadow: [
439
+ BoxShadow(
440
+ color: Colors.black.withValues(alpha: context.isDark ? 0.45 : 0.10),
441
+ blurRadius: 24,
442
+ offset: const Offset(0, 10),
443
+ ),
444
+ ],
445
+ ),
446
+ child: Row(
447
+ crossAxisAlignment: CrossAxisAlignment.start,
448
+ children: [
449
+ _AppIconBadge(name: name),
450
+ const SizedBox(width: 10),
451
+ Expanded(
452
+ child: Column(
453
+ crossAxisAlignment: CrossAxisAlignment.start,
454
+ mainAxisSize: MainAxisSize.min,
455
+ children: [
456
+ Row(
457
+ children: [
458
+ Expanded(
459
+ child: Text(
460
+ name.toUpperCase(),
461
+ maxLines: 1,
462
+ overflow: TextOverflow.ellipsis,
463
+ style: context.textTheme.labelSmall?.copyWith(
464
+ color: context.colors.muted,
465
+ letterSpacing: 0.4,
466
+ fontWeight: FontWeight.w600,
467
+ ),
468
+ ),
469
+ ),
470
+ const SizedBox(width: 6),
471
+ Text(
472
+ tr.send_push_preview_now,
473
+ style: context.textTheme.labelSmall?.copyWith(
474
+ color: context.colors.muted,
475
+ ),
476
+ ),
477
+ ],
478
+ ),
479
+ const SizedBox(height: 3),
480
+ Text(
481
+ titleEmpty ? tr.send_push_preview_title_placeholder : title.trim(),
482
+ maxLines: 1,
483
+ overflow: TextOverflow.ellipsis,
484
+ style: context.textTheme.bodyMedium?.copyWith(
485
+ color: titleEmpty
486
+ ? context.colors.muted
487
+ : context.colors.onSurface,
488
+ fontWeight: FontWeight.w600,
489
+ ),
490
+ ),
491
+ const SizedBox(height: 2),
492
+ Text(
493
+ bodyEmpty ? tr.send_push_preview_body_placeholder : body.trim(),
494
+ maxLines: 2,
495
+ overflow: TextOverflow.ellipsis,
496
+ style: context.textTheme.bodySmall?.copyWith(
497
+ color: context.colors.muted,
498
+ height: 1.3,
499
+ ),
500
+ ),
501
+ ],
502
+ ),
503
+ ),
504
+ if (showImage) ...[
505
+ const SizedBox(width: 10),
506
+ _PreviewThumb(url: url),
507
+ ],
508
+ ],
509
+ ),
510
+ );
511
+ }
512
+ }
513
+
514
+ /// Rounded-square app icon (iOS app-icon look). Uses the REAL launcher icon
515
+ /// (`assets/images/icon.png`) so the preview always mirrors the app's actual
516
+ /// logo — change the icon and this follows. Falls back to a tinted square with
517
+ /// the app's initial if the asset can't be loaded.
518
+ class _AppIconBadge extends StatelessWidget {
519
+ final String name;
520
+ const _AppIconBadge({required this.name});
521
+
522
+ @override
523
+ Widget build(BuildContext context) {
524
+ return ClipRRect(
525
+ borderRadius: BorderRadius.circular(10),
526
+ child: Image.asset(
527
+ 'assets/images/icon.png',
528
+ width: 40,
529
+ height: 40,
530
+ fit: BoxFit.cover,
531
+ errorBuilder: (context, error, stack) => _fallback(context),
532
+ ),
533
+ );
534
+ }
535
+
536
+ Widget _fallback(BuildContext context) {
537
+ final String letter =
538
+ name.isNotEmpty ? name.substring(0, 1).toUpperCase() : 'A';
539
+ return Container(
540
+ width: 40,
541
+ height: 40,
542
+ decoration: BoxDecoration(
543
+ gradient: LinearGradient(
544
+ begin: Alignment.topLeft,
545
+ end: Alignment.bottomRight,
546
+ colors: [
547
+ context.colors.primary,
548
+ context.colors.primary.withValues(alpha: 0.7),
549
+ ],
550
+ ),
551
+ borderRadius: BorderRadius.circular(10),
552
+ ),
553
+ alignment: Alignment.center,
554
+ child: Text(
555
+ letter,
556
+ style: context.textTheme.titleMedium?.copyWith(
557
+ color: context.colors.onPrimary,
558
+ fontWeight: FontWeight.w800,
559
+ ),
560
+ ),
561
+ );
562
+ }
563
+ }
564
+
565
+ /// Thumbnail of the notification image, loaded live from the URL. Keeps the old
566
+ /// frame while a new URL loads (gaplessPlayback) and shows a fallback on error.
567
+ class _PreviewThumb extends StatelessWidget {
568
+ final String url;
569
+ const _PreviewThumb({required this.url});
570
+
571
+ @override
572
+ Widget build(BuildContext context) {
573
+ return ClipRRect(
574
+ borderRadius: BorderRadius.circular(8),
575
+ child: SizedBox(
576
+ width: 40,
577
+ height: 40,
578
+ child: Image.network(
579
+ url,
580
+ fit: BoxFit.cover,
581
+ gaplessPlayback: true,
582
+ loadingBuilder: (context, child, progress) =>
583
+ progress == null ? child : _placeholder(context, loading: true),
584
+ errorBuilder: (context, error, stack) =>
585
+ _placeholder(context, loading: false),
586
+ ),
587
+ ),
588
+ );
589
+ }
590
+
591
+ Widget _placeholder(BuildContext context, {required bool loading}) {
592
+ return Container(
593
+ color: context.colors.surfaceNeutralSoft,
594
+ alignment: Alignment.center,
595
+ child: Icon(
596
+ loading ? Icons.image_outlined : Icons.broken_image_outlined,
597
+ size: 18,
598
+ color: context.colors.muted,
599
+ ),
600
+ );
601
+ }
602
+ }
@@ -354,7 +354,7 @@ class _AvatarContent extends StatelessWidget {
354
354
 
355
355
  @override
356
356
  Widget build(BuildContext context) {
357
- // 1. Arquivo temporário escolhido da galeria (prioridade máxima)
357
+ // 1. Temporary file picked from the gallery (highest priority)
358
358
  if (localImage != null) {
359
359
  return KasyAvatar(
360
360
  diameter: diameter,
@@ -376,7 +376,7 @@ class _AvatarContent extends StatelessWidget {
376
376
 
377
377
  final id = userId;
378
378
  if (id != null) {
379
- // 2. URL em cache: síncrono, sem FutureBuilder, sem flash de fallback
379
+ // 2. Cached URL: synchronous, no FutureBuilder, no fallback flash
380
380
  final cachedUrl = getCachedAvatarUrl(id);
381
381
  if (cachedUrl != null && cachedUrl.isNotEmpty) {
382
382
  return KasyAvatar(
@@ -387,7 +387,7 @@ class _AvatarContent extends StatelessWidget {
387
387
  );
388
388
  }
389
389
 
390
- // 3. URL não está em cache: busca async (primeiro acesso ou após upload)
390
+ // 3. URL not cached: async fetch (first access or after upload)
391
391
  return FutureBuilder<String>(
392
392
  future: resolveAvatarUrl(id),
393
393
  builder: (context, snapshot) {
@@ -0,0 +1,77 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:kasy_kit/components/kasy_avatar.dart';
4
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
5
+ import 'package:kasy_kit/core/data/models/user.dart';
6
+ import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
+ import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
8
+
9
+ /// The signed-in user's avatar, resolved through the exact same path as the
10
+ /// settings screen — so it behaves identically across the **Firebase, Supabase
11
+ /// and API** backends. [avatar_utils] (the URL resolver) is swapped per backend
12
+ /// by the CLI, and the user identity comes from [userStateNotifierProvider]
13
+ /// (backend-agnostic), so this widget never references a specific backend.
14
+ ///
15
+ /// Resolution order: the user's `avatarPath` URL → the in-memory cache →
16
+ /// the backend's avatar store (async). Falls back to a [gradient] fill while
17
+ /// loading, for guests, or when no photo exists.
18
+ class KasyUserAvatar extends ConsumerWidget {
19
+ const KasyUserAvatar({
20
+ super.key,
21
+ this.diameter = 36,
22
+ this.gradient = KasyAvatarGradients.indigo,
23
+ this.onTap,
24
+ });
25
+
26
+ /// Avatar diameter in logical pixels.
27
+ final double diameter;
28
+
29
+ /// Gradient shown when the user has no photo (or while it loads).
30
+ final KasyAvatarGradientData gradient;
31
+
32
+ /// Tap handler (open account menu / profile). Null = inert.
33
+ final VoidCallback? onTap;
34
+
35
+ KasyAvatar _avatar([ImageProvider? image]) => KasyAvatar(
36
+ diameter: diameter,
37
+ image: image,
38
+ backgroundGradient: gradient,
39
+ onTap: onTap,
40
+ );
41
+
42
+ @override
43
+ Widget build(BuildContext context, WidgetRef ref) {
44
+ final User user = ref.watch(userStateNotifierProvider).user;
45
+
46
+ // 1. A direct URL on the user record (social logins; Supabase/API users).
47
+ final String? avatarPath = switch (user) {
48
+ AuthenticatedUserData(:final avatarPath) => avatarPath,
49
+ _ => null,
50
+ };
51
+ if (avatarPath != null && avatarPath.isNotEmpty) {
52
+ return _avatar(NetworkImage(avatarPath));
53
+ }
54
+
55
+ final String? userId = user.idOrNull;
56
+ if (userId != null) {
57
+ // 2. Cached URL — synchronous, avoids a fallback flash.
58
+ final String? cached = getCachedAvatarUrl(userId);
59
+ if (cached != null && cached.isNotEmpty) {
60
+ return _avatar(NetworkImage(cached));
61
+ }
62
+ // 3. Resolve through the backend's avatar store (async, per-backend).
63
+ return FutureBuilder<String>(
64
+ future: resolveAvatarUrl(userId),
65
+ builder: (context, snapshot) {
66
+ final String? url = snapshot.data;
67
+ return _avatar(
68
+ (url != null && url.isNotEmpty) ? NetworkImage(url) : null,
69
+ );
70
+ },
71
+ );
72
+ }
73
+
74
+ // 4. Guest / loading → gradient fill.
75
+ return _avatar();
76
+ }
77
+ }
@@ -156,6 +156,7 @@ class SettingsTile extends StatelessWidget {
156
156
  return KasyHover(
157
157
  onTap: onTap,
158
158
  hoverEnabled: false,
159
+ pressEnabled: false,
159
160
  semanticLabel: title,
160
161
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
161
162
  child: Row(
@@ -15,6 +15,19 @@ enum SubscriptionStatus {
15
15
  CANCELLED,
16
16
  }
17
17
 
18
+ /// Where the subscription was purchased (its origin). Used to route the user to
19
+ /// the correct management flow regardless of the device they are currently on.
20
+ /// `STRIPE` means the subscription was bought on the web via Stripe.
21
+ enum SubscriptionStore {
22
+ PLAY_STORE,
23
+ APPLE_STORE,
24
+ EARLY_BIRD,
25
+ STRIPE,
26
+ // Forward-compat: any value the backend sends that we don't know yet maps
27
+ // here, so parsing never throws and routing safely falls back to platform.
28
+ unknown,
29
+ }
30
+
18
31
 
19
32
  @freezed
20
33
  sealed class SubscriptionEntity with _$SubscriptionEntity {
@@ -32,6 +45,10 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
32
45
  @TimestampConverter()
33
46
  DateTime? periodEndDate,
34
47
  @JsonKey(name: 'status') required SubscriptionStatus status,
48
+ // Origin of the subscription (apple/play/stripe/...). Nullable + tolerant
49
+ // parsing so old records (no `store`) and future values never break.
50
+ @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
51
+ SubscriptionStore? store,
35
52
  }) = SubscriptionEntityData;
36
53
 
37
54
  factory SubscriptionEntity.fromJson(String id, Map<String, Object?> json) =>
@@ -1,21 +1,22 @@
1
1
  import 'package:flutter/foundation.dart';
2
- import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+ import 'package:flutter/services.dart' show PlatformException;
3
+ import 'package:kasy_kit/core/data/models/entitlement.dart';
3
4
  import 'package:kasy_kit/core/data/models/subscription.dart';
4
- import 'package:kasy_kit/environnements.dart';
5
+ import 'package:kasy_kit/environments.dart';
6
+ import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
7
+ import 'package:kasy_kit/features/subscriptions/api/revenuecat_product.dart';
8
+ import 'package:kasy_kit/features/subscriptions/api/subscription_payment_api.dart';
5
9
  import 'package:logger/logger.dart';
6
10
  import 'package:purchases_flutter/purchases_flutter.dart';
7
11
  import 'package:universal_io/io.dart';
8
12
  import 'package:url_launcher/url_launcher.dart';
9
13
 
10
- final inAppSubscriptionApiProvider = Provider(
11
- (ref) => RevenueCatPaymentApi(environment: ref.read(environmentProvider)),
12
- );
13
-
14
- // We chose to use RevenueCat for in-app subscription
15
- // but you can use any other provider (RevenueCat, ...)
14
+ // We chose to use RevenueCat for in-app subscription (mobile: App Store / Play
15
+ // Store). Web subscriptions are handled by the separate Stripe module via
16
+ // [StripePaymentApi]; both implement [SubscriptionPaymentApi].
16
17
  // ---
17
18
  // We wrap the RevenueCat API to be able to fake it properly (Glassfy use static methods which are hard for tests)
18
- class RevenueCatPaymentApi {
19
+ class RevenueCatPaymentApi implements SubscriptionPaymentApi {
19
20
  bool _hasInit = false;
20
21
  final Environment _environment;
21
22
 
@@ -30,16 +31,7 @@ class RevenueCatPaymentApi {
30
31
  return key.startsWith('test_');
31
32
  }
32
33
 
33
- /// Web Billing sandbox keys (`rcb_sb_`) must not ship in release.
34
- static bool isRevenueCatWebSandboxKey(String? key) {
35
- if (key == null || key.isEmpty) return false;
36
- return key.startsWith('rcb_sb_');
37
- }
38
-
39
34
  String? _apiKeyForCurrentPlatform() {
40
- if (kIsWeb) {
41
- return _environment.revenueCatWebApiKey;
42
- }
43
35
  if (Platform.isAndroid) {
44
36
  return _environment.revenueCatAndroidApiKey;
45
37
  }
@@ -52,13 +44,26 @@ class RevenueCatPaymentApi {
52
44
  bool _mustSkipConfigureDueToSandboxKeyInRelease() {
53
45
  if (!kReleaseMode) return false;
54
46
  final String? key = _apiKeyForCurrentPlatform();
55
- if (kIsWeb) {
56
- return isRevenueCatWebSandboxKey(key);
57
- }
58
47
  return isRevenueCatTestStorePublicKey(key);
59
48
  }
60
49
 
50
+ /// Map a RevenueCat [EntitlementInfo] to the provider-agnostic [Entitlement].
51
+ Entitlement _toEntitlement(EntitlementInfo e) => Entitlement(
52
+ identifier: e.identifier,
53
+ isActive: e.isActive,
54
+ isInTrial: e.periodType == PeriodType.trial,
55
+ willRenew: e.willRenew,
56
+ productId: e.productIdentifier,
57
+ expirationDate: e.expirationDate != null
58
+ ? DateTime.tryParse(e.expirationDate!)
59
+ : null,
60
+ );
61
+
62
+ @override
61
63
  Future<void> init() async {
64
+ // RevenueCat is mobile-only (Android/iOS). Web subscriptions are handled by
65
+ // the Stripe module when present; never configure RevenueCat on web.
66
+ if (kIsWeb) return;
62
67
  if (!_environment.isRevenueCatConfigured) {
63
68
  Logger().w('=== RevenueCat API key not provided ===');
64
69
  Logger().w(
@@ -77,15 +82,7 @@ class RevenueCatPaymentApi {
77
82
  }
78
83
  await Purchases.setLogLevel(kDebugMode ? LogLevel.debug : LogLevel.warn);
79
84
  PurchasesConfiguration configuration;
80
- if (kIsWeb) {
81
- final webKey = _environment.revenueCatWebApiKey;
82
- if (webKey == null || webKey.isEmpty) {
83
- throw Exception(
84
- 'RevenueCat Web API key not configured. Set RC_WEB_API_KEY.',
85
- );
86
- }
87
- configuration = PurchasesConfiguration(webKey);
88
- } else if (Platform.isAndroid) {
85
+ if (Platform.isAndroid) {
89
86
  configuration = PurchasesConfiguration(
90
87
  _environment.revenueCatAndroidApiKey!,
91
88
  );
@@ -99,6 +96,7 @@ class RevenueCatPaymentApi {
99
96
  }
100
97
 
101
98
  // We use a custom subscriber id to be able to identify the user
99
+ @override
102
100
  Future<void> initUser(String userId) async {
103
101
  if (kDebugMode && !_environment.isRevenueCatConfigured) {
104
102
  Logger().w('=== RevenueCat API key not provided ===');
@@ -115,6 +113,7 @@ class RevenueCatPaymentApi {
115
113
  }
116
114
 
117
115
  // We can dissociate the user from the subscription
116
+ @override
118
117
  Future<void> disconnectUser() async {
119
118
  if (!_hasInit) {
120
119
  return;
@@ -122,6 +121,7 @@ class RevenueCatPaymentApi {
122
121
  await Purchases.logOut();
123
122
  }
124
123
 
124
+ @override
125
125
  Future<List<SubscriptionProduct>> getOffers(String? offerId) async {
126
126
  if (kDebugMode && !_environment.isRevenueCatConfigured) {
127
127
  Logger().d(
@@ -211,6 +211,25 @@ class RevenueCatPaymentApi {
211
211
  return Purchases.purchase(purchaseParams);
212
212
  }
213
213
 
214
+ @override
215
+ Future<void> purchaseProduct(SubscriptionProduct product) async {
216
+ if (product is! RevenueCatProduct) {
217
+ throw ArgumentError(
218
+ 'RevenueCatPaymentApi can only purchase a RevenueCatProduct',
219
+ );
220
+ }
221
+ try {
222
+ await purchasePackage(product.revenueCatPackage);
223
+ } on PlatformException catch (e) {
224
+ final error = PurchasesErrorHelper.getErrorCode(e);
225
+ if (error == PurchasesErrorCode.purchaseCancelledError) {
226
+ throw UserCancelledPurchaseException();
227
+ }
228
+ rethrow;
229
+ }
230
+ }
231
+
232
+ @override
214
233
  Future<void> presentCodeRedemptionSheet() {
215
234
  if (TargetPlatform.iOS != defaultTargetPlatform) {
216
235
  throw "Only available on iOS";
@@ -221,12 +240,11 @@ class RevenueCatPaymentApi {
221
240
  return Purchases.presentCodeRedemptionSheet();
222
241
  }
223
242
 
224
- Future<void> unsubscribe() async {
225
- if (kIsWeb) {
226
- // Web Billing: user manages subscription via RevenueCat customer portal
227
- // or the checkout flow. No-op here; UI can show a link to the portal.
228
- return;
229
- }
243
+ @override
244
+ Future<void> unsubscribe(SubscriptionStore? origin) async {
245
+ // RevenueCat only manages App Store / Play Store subscriptions, and only on
246
+ // the matching device. The caller (provider) already decided this is the
247
+ // right place to manage; here we just open the current platform's store.
230
248
  if (Platform.isAndroid) {
231
249
  launchUrl(
232
250
  Uri.parse("https://play.google.com/store/account/subscriptions"),
@@ -266,6 +284,7 @@ class RevenueCatPaymentApi {
266
284
  return result;
267
285
  }
268
286
 
287
+ @override
269
288
  Future<SubscriptionProduct?> getFromProductId(String productId) async {
270
289
  if (!_hasInit) {
271
290
  return null;
@@ -323,10 +342,10 @@ class RevenueCatPaymentApi {
323
342
  return products;
324
343
  }
325
344
 
345
+ @override
326
346
  Future<void> restorePurchase() async {
327
347
  if (kIsWeb) {
328
- // Restore is not supported on web; Web Billing manages subscriptions
329
- // via the customer portal. Return without error.
348
+ // Restore is not supported on web for RevenueCat. Return without error.
330
349
  return;
331
350
  }
332
351
  if (!_hasInit) {
@@ -338,20 +357,26 @@ class RevenueCatPaymentApi {
338
357
  }
339
358
  }
340
359
 
341
- Future<List<EntitlementInfo>> getPermissions() async {
360
+ @override
361
+ Future<List<Entitlement>> getPermissions() async {
342
362
  if (!_hasInit) {
343
363
  return [];
344
364
  }
345
365
  final customerInfo = await Purchases.getCustomerInfo();
346
- return customerInfo.entitlements.active.values.toList();
366
+ return customerInfo.entitlements.active.values
367
+ .map(_toEntitlement)
368
+ .toList();
347
369
  }
348
370
 
349
- Future<List<EntitlementInfo>?> getEntitlements() async {
371
+ @override
372
+ Future<List<Entitlement>?> getEntitlements() async {
350
373
  if (!_hasInit) {
351
374
  return [];
352
375
  }
353
376
  final customerInfo = await Purchases.getCustomerInfo();
354
- return customerInfo.entitlements.active.values.toList();
377
+ return customerInfo.entitlements.active.values
378
+ .map(_toEntitlement)
379
+ .toList();
355
380
  }
356
381
  }
357
382
 
@@ -383,7 +408,3 @@ class SubscriptionProductAdapter {
383
408
  }
384
409
  }
385
410
  }
386
-
387
- class UserCancelledPurchaseException implements Exception {
388
- UserCancelledPurchaseException();
389
- }