kasy-cli 1.31.14 → 1.34.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 (127) hide show
  1. package/bin/kasy.js +42 -0
  2. package/lib/commands/apple-web.js +222 -0
  3. package/lib/commands/configure.js +3 -91
  4. package/lib/commands/doctor.js +20 -0
  5. package/lib/commands/facebook.js +189 -0
  6. package/lib/commands/new.js +65 -3
  7. package/lib/scaffold/CHANGELOG.json +27 -0
  8. package/lib/scaffold/backends/api/patch/README.md +87 -2
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  11. package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
  12. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  13. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  14. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  15. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  16. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
  18. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  19. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  20. package/lib/scaffold/generate.js +1 -1
  21. package/lib/scaffold/shared/generator-utils.js +34 -3
  22. package/lib/utils/apple-web.js +147 -0
  23. package/lib/utils/facebook.js +162 -0
  24. package/lib/utils/i18n/messages-en.js +64 -0
  25. package/lib/utils/i18n/messages-es.js +64 -0
  26. package/lib/utils/i18n/messages-pt.js +64 -0
  27. package/package.json +2 -2
  28. package/templates/firebase/AGENTS.md +87 -0
  29. package/templates/firebase/CLAUDE.md +16 -0
  30. package/templates/firebase/DESIGN_SYSTEM.md +234 -0
  31. package/templates/firebase/docs/auth-setup.en.md +7 -1
  32. package/templates/firebase/docs/auth-setup.es.md +7 -1
  33. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  34. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  35. package/templates/firebase/lib/components/components.dart +1 -0
  36. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  37. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  38. package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
  39. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  40. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  41. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  42. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  43. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  44. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  45. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  46. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  47. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  48. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  49. package/templates/firebase/lib/components/kasy_toast.dart +39 -70
  50. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  51. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  52. package/templates/firebase/lib/core/config/features.dart +18 -0
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  54. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  55. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
  56. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  57. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  58. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  59. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  60. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  61. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  62. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  63. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  66. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  67. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
  68. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  69. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  70. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  71. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
  72. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
  73. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  74. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  75. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  76. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
  77. package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
  78. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
  79. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  80. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
  81. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  82. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  83. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  84. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
  85. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  86. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
  87. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  88. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  89. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  90. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  91. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  92. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
  93. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  94. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
  95. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
  96. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  97. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  98. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  99. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  100. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  101. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  102. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  103. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  104. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  105. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  106. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  107. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  108. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  109. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  110. package/templates/firebase/lib/i18n/en.i18n.json +13 -4
  111. package/templates/firebase/lib/i18n/es.i18n.json +13 -4
  112. package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
  113. package/templates/firebase/lib/router.dart +2 -0
  114. package/templates/firebase/pubspec.yaml +1 -2
  115. package/templates/firebase/tool/design_check.dart +152 -0
  116. package/templates/firebase/web/stripe_success.html +64 -26
  117. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  118. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  119. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  120. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  121. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  122. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  123. package/templates/firebase/assets/images/review.png +0 -0
  124. package/templates/firebase/assets/images/update.png +0 -0
  125. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  126. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
  127. package/templates/firebase/login-redesign-preview.png +0 -0
@@ -44,8 +44,10 @@ const { generateApiProject } = require('../scaffold/backends/api/generator');
44
44
  const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys, classifyCreateError } = require('../scaffold/backends/supabase/deploy');
45
45
  const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, ensureGoogleServiceInfoPlist, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
46
46
  const { toPackageName } = require('../scaffold/backends/firebase/tokens');
47
- const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
47
+ const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, authorizeLocalhostForProject, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
48
48
  const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
49
+ const { autoApplyAppleWebIfCached } = require('./apple-web');
50
+ const { autoApplyFacebookIfCached } = require('./facebook');
49
51
  const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
50
52
 
51
53
  // Default region for creating Supabase projects via the API.
@@ -481,7 +483,7 @@ function printCreateFromScratchStatus(result, tr) {
481
483
  }
482
484
  }
483
485
 
484
- function printSuccessCard(tr, answers, targetDir) {
486
+ function printSuccessCard(tr, answers, targetDir, appleWebStatus = null, facebookStatus = null) {
485
487
  const folderName = path.basename(targetDir);
486
488
 
487
489
  const lines = [];
@@ -509,6 +511,27 @@ function printSuccessCard(tr, answers, targetDir) {
509
511
  }
510
512
  }
511
513
 
514
+ if (appleWebStatus === 'configured') {
515
+ lines.push(paintLime(`✓ ${tr('new.success.appleWeb.configured')}`));
516
+ lines.push('');
517
+ } else if (appleWebStatus === 'pending') {
518
+ lines.push(kleur.yellow(`! ${tr('new.success.appleWeb.pending')}`));
519
+ lines.push('');
520
+ }
521
+
522
+ if (facebookStatus === 'configured') {
523
+ lines.push(paintLime(`✓ ${tr('new.success.facebook.configured')}`));
524
+ lines.push('');
525
+ } else if (facebookStatus === 'pending') {
526
+ lines.push(kleur.yellow(`! ${tr('new.success.facebook.pending')}`));
527
+ lines.push('');
528
+ }
529
+
530
+ if (answers.backend === 'api') {
531
+ lines.push(kleur.yellow(`! ${tr('new.success.api.serverContracts')}`));
532
+ lines.push('');
533
+ }
534
+
512
535
  lines.push(kleur.bold(tr('new.success.nextSteps')));
513
536
  lines.push('');
514
537
 
@@ -1827,6 +1850,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1827
1850
  // Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
1828
1851
  // Auth untouched, so initialize it here (idempotent) before the deploy.
1829
1852
  await ensureFirebaseAuthInitialized(answers.firebaseProjectId);
1853
+ // Web Google sign-in on Supabase brokers the Google ID token through the
1854
+ // Firebase popup (signInWithPopup), which only runs from an authorized
1855
+ // domain. fcmOnly setup skips the authorizedDomains step, so localhost is
1856
+ // missing here and the web popup dies with [firebase_auth/unauthorized-domain].
1857
+ // Add it best-effort now that auth is initialized. Native (mobile) is unaffected.
1858
+ const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
1859
+ if (!localhostDomains.ok) {
1860
+ ui.log.warn(tr('new.google.localhostDomainWarn'));
1861
+ }
1830
1862
  const cliResult = await enableAuthViaFirebaseCli({
1831
1863
  projectDir: targetDir,
1832
1864
  projectId: answers.firebaseProjectId,
@@ -2104,7 +2136,37 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
2104
2136
  // APNs key (iOS push) is intentionally not mentioned here — it only becomes
2105
2137
  // relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
2106
2138
 
2107
- printSuccessCard(tr, answers, targetDir);
2139
+ // Apple web: auto-configure if the developer saved credentials on a previous
2140
+ // project; otherwise it stays pending and the success card points to the command.
2141
+ let appleWebStatus = null;
2142
+ try {
2143
+ const aw = await autoApplyAppleWebIfCached(targetDir);
2144
+ if (aw.applied) {
2145
+ appleWebStatus = 'configured';
2146
+ ui.log.success(tr('new.appleWeb.autoConfigured'));
2147
+ } else if (aw.pending) {
2148
+ appleWebStatus = 'pending';
2149
+ }
2150
+ } catch (_) {
2151
+ // Non-fatal: web Apple is optional and can be configured later.
2152
+ }
2153
+
2154
+ // Facebook: auto-configure if it's enabled in the project and credentials were
2155
+ // saved on a previous project; otherwise pending and the card points to the command.
2156
+ let facebookStatus = null;
2157
+ try {
2158
+ const fb = await autoApplyFacebookIfCached(targetDir);
2159
+ if (fb.applied) {
2160
+ facebookStatus = 'configured';
2161
+ ui.log.success(tr('new.facebook.autoConfigured'));
2162
+ } else if (fb.pending) {
2163
+ facebookStatus = 'pending';
2164
+ }
2165
+ } catch (_) {
2166
+ // Non-fatal: Facebook is optional and can be configured later.
2167
+ }
2168
+
2169
+ printSuccessCard(tr, answers, targetDir, appleWebStatus, facebookStatus);
2108
2170
 
2109
2171
  ui.outro(kleur.bold(tr('new.success.title')));
2110
2172
  }
@@ -1,4 +1,31 @@
1
1
  {
2
+ "1.34.0": {
3
+ "modules": {
4
+ "core": {
5
+ "pt": "Login com Facebook automatizado + Facebook na web (Firebase): novo comando `kasy facebook` guia os passos na Meta (abre o link), grava App ID + Client Token no iOS/Android e habilita o provedor no Firebase (Identity Toolkit) ou Supabase. No Firebase, o Facebook passa a funcionar na WEB (signInWithPopup) e o botão só aparece quando configurado (flag withFacebookWebSignin). Apple/Facebook na web no Supabase ficam como native-only (roadmap). Corrigido: o `kasy apple-web` agora liga o Apple web só no Firebase (não cria mais botão morto no Supabase). Credenciais ficam salvas e o `kasy new` aplica sozinho.",
6
+ "en": "Facebook Login automated + Facebook on web (Firebase): new `kasy facebook` command guides the Meta steps (opens the link), writes App ID + Client Token into iOS/Android and enables the provider on Firebase (Identity Toolkit) or Supabase. On Firebase, Facebook now works on the WEB (signInWithPopup) and the button only shows once configured (withFacebookWebSignin flag). Apple/Facebook on web for Supabase stay native-only (roadmap). Fixed: `kasy apple-web` now enables Apple web on Firebase only (no more dead button on Supabase). Credentials are cached and `kasy new` applies them automatically.",
7
+ "es": "Inicio de sesión con Facebook automatizado + Facebook en la web (Firebase): nuevo comando `kasy facebook` guía los pasos en Meta (abre el enlace), escribe App ID + Client Token en iOS/Android y habilita el proveedor en Firebase (Identity Toolkit) o Supabase. En Firebase, Facebook ahora funciona en la WEB (signInWithPopup) y el botón solo aparece cuando está configurado (flag withFacebookWebSignin). Apple/Facebook en la web para Supabase quedan como native-only (roadmap). Corregido: `kasy apple-web` ahora activa Apple web solo en Firebase (sin botón muerto en Supabase). Las credenciales se guardan y `kasy new` las aplica automáticamente."
8
+ }
9
+ }
10
+ },
11
+ "1.33.0": {
12
+ "modules": {
13
+ "core": {
14
+ "pt": "Login com Apple na WEB agora é automatizável (backend Firebase): novo comando `kasy apple-web` grava o codeFlowConfig (Service ID + Team ID + Key ID + .p8) no provedor Apple do Firebase, que re-assina o secret sozinho (não expira), reaproveitando suas credenciais salvas. Projetos Firebase novos já nascem com Apple web se você já configurou antes. O botão Apple na web só aparece quando funciona de verdade (sem botão morto); `kasy doctor` mostra se falta configurar. No Supabase, Apple na web é roadmap.",
15
+ "en": "Apple Sign-In on the WEB is now automatable (Firebase backend): new `kasy apple-web` command writes the codeFlowConfig (Service ID + Team ID + Key ID + .p8) into the Firebase Apple provider, which re-signs the secret itself (never expires), reusing your saved credentials. New Firebase projects ship web Apple ready if you configured it before. The web Apple button only shows when it actually works (no dead button); `kasy doctor` reports if it's pending. On Supabase, Apple on web is roadmap.",
16
+ "es": "El inicio de sesión con Apple en la WEB ahora es automatizable (backend Firebase): nuevo comando `kasy apple-web` escribe el codeFlowConfig (Service ID + Team ID + Key ID + .p8) en el proveedor Apple de Firebase, que vuelve a firmar el secret solo (no expira), reutilizando tus credenciales guardadas. Los proyectos Firebase nuevos vienen con Apple web listo si ya lo configuraste antes. El botón de Apple en la web solo aparece cuando funciona de verdad (sin botón muerto); `kasy doctor` indica si falta configurar. En Supabase, Apple en la web es roadmap."
17
+ }
18
+ }
19
+ },
20
+ "1.32.0": {
21
+ "modules": {
22
+ "core": {
23
+ "pt": "Proporção da web agora se adapta à largura real da janela: telas com escala alta do sistema (Windows em 125/150/175%) não ficam mais cortadas (sidebar/header), e o Mac continua igual. Ajuste só na web, nativo intocado.",
24
+ "en": "Web proportion now adapts to the real window width: high system-scale displays (Windows at 125/150/175%) no longer look cropped (sidebar/header), and Mac stays the same. Web-only, native untouched.",
25
+ "es": "La proporción en web ahora se adapta al ancho real de la ventana: pantallas con escala alta del sistema (Windows al 125/150/175%) ya no se ven recortadas (sidebar/header), y Mac queda igual. Solo en web, nativo intacto."
26
+ }
27
+ }
28
+ },
2
29
  "1.31.12": {
3
30
  "modules": {
4
31
  "core": {
@@ -37,6 +37,7 @@ Ficam no `.vscode/launch.json` como variáveis de ambiente de build (`--dart-def
37
37
  | `RC_ANDROID_PROD_KEY` | RevenueCat | Dashboard RevenueCat → Apps → Google Play (chave `goog_`, usada automaticamente em Android físico) |
38
38
  | `SENTRY_DSN` | Sentry | Dashboard Sentry → Projeto → DSN |
39
39
  | `MIXPANEL_TOKEN` | Mixpanel | Dashboard Mixpanel → Configurações → Token |
40
+ | `AI_CHAT_ENDPOINT` | IA Chat | URL do seu endpoint SSE de chat (ex.: `https://sua-api/ai-chat`) |
40
41
 
41
42
  Para atualizar, edite o `.vscode/launch.json`.
42
43
 
@@ -157,8 +158,92 @@ Formato de uma conversa (o "última mensagem" é desnormalizado para a lista fic
157
158
  ```
158
159
 
159
160
  Ao salvar uma mensagem, o servidor deve atualizar `updated_at`, `last_message_role` e
160
- `last_message_content` da conversa. O streaming da resposta da IA continua no endpoint
161
- `AI_CHAT_ENDPOINT` (SSE) — ele só recebe `message` + `history`, não persiste nada.
161
+ `last_message_content` da conversa.
162
+
163
+ ### Endpoint: AI Chat (resposta em streaming)
164
+
165
+ A resposta da IA é transmitida palavra a palavra via SSE por um endpoint separado,
166
+ cuja URL vem do `--dart-define=AI_CHAT_ENDPOINT=...` (quadro de credenciais acima).
167
+ Este endpoint **não persiste nada** — só faz o proxy para o provedor (OpenAI/Gemini)
168
+ e devolve o texto em stream. A chave do provedor fica **só no servidor**.
169
+
170
+ ```
171
+ POST {AI_CHAT_ENDPOINT}
172
+ Auth: Authorization: Bearer <token> (enviado automaticamente pelo app)
173
+ Content-Type: application/json
174
+ body:
175
+ {
176
+ "message": "última mensagem do usuário",
177
+ "history": [ { "role": "user" | "assistant", "content": "..." } ]
178
+ }
179
+
180
+ 200 OK (text/event-stream)
181
+ → devolva o texto da resposta em chunks (stream); o app concatena e renderiza
182
+ em tempo real. Mantenha a chave da IA (OPENAI/GEMINI) apenas no servidor.
183
+ ```
184
+
185
+ Se `AI_CHAT_ENDPOINT` não estiver definido, o chat mostra o estado "não configurado"
186
+ (o app não quebra). Referência pronta: a Edge Function `ai-chat` do backend Supabase.
187
+
188
+ ---
189
+
190
+ ## Excluir conta
191
+
192
+ O app chama um endpoint para o usuário excluir a própria conta. **É obrigatório
193
+ para publicar na App Store e na Play Store**, então o seu backend precisa implementá-lo.
194
+
195
+ ```
196
+ DELETE /users/me
197
+ Auth: Authorization: Bearer <token> (identifica o usuário; enviado pelo app)
198
+
199
+ O servidor DEVE:
200
+ 1. Apagar o usuário do sistema de auth para que aquele login nunca mais entre.
201
+ 2. Apagar em cascata TODOS os dados do usuário: perfil, devices/tokens de push,
202
+ conversas + mensagens de IA, votos de feature requests, assinaturas, avatar.
203
+ → responda 2xx em caso de sucesso.
204
+ ```
205
+
206
+ Sem esse endpoint, a exclusão de conta falha silenciosamente (404/405) num projeto
207
+ API novo.
208
+
209
+ ---
210
+
211
+ ## Notificações push (FCM)
212
+
213
+ Push **nativo (Android/iOS)** depende do seu servidor: o app registra o token do
214
+ device e o seu backend envia via **FCM HTTP v1**. Na **web**, push é no-op de propósito
215
+ (o app não registra token e não tenta enviar — só mostra as notificações que já existem).
216
+
217
+ A chave de Service Account do Firebase foi salva pelo `kasy new` em
218
+ `.kasy/fcm-service-account.json`. Carregue-a no servidor (ex.: variável
219
+ `FIREBASE_SERVICE_ACCOUNT_JSON`) e use-a para chamar a FCM HTTP v1. Referência pronta
220
+ de implementação: a Edge Function `send-push-notification` do backend Supabase.
221
+
222
+ ### Endpoints: devices (tokens de push)
223
+
224
+ ```
225
+ POST /users/{userId}/devices → registra/atualiza um device (body com token, platform, etc.)
226
+ PUT /devices/{deviceId} → atualiza um device existente
227
+ DELETE /devices/{deviceId} → remove um device
228
+ PATCH /users/{userId}/devices/{installationId}/touch → marca o device como ativo agora (last-seen)
229
+ POST /users/{userId}/devices/cleanup-stale → remove devices antigos/inválidos
230
+ DELETE /users/{userId}/devices → remove todos os devices do usuário (ex.: no logout)
231
+ ```
232
+
233
+ ### Endpoints: notifications
234
+
235
+ ```
236
+ GET /users/{userId}/notifications?page=&pageSize= → lista paginada, mais recente primeiro
237
+ PUT /users/{userId}/notifications/{id} → marca como lida
238
+ DELETE /users/{userId}/notifications/{id} → apaga uma notificação
239
+ GET /users/{userId}/notifications/unread → SSE: stream da contagem de não-lidas (alimenta a "bolinha")
240
+ POST /users/{userId}/notifications → cria/envia para UM usuário (body: title, body, image_url?, data.route?, type)
241
+ POST /notifications/broadcast → envia para TODOS (mesmo body)
242
+ ```
243
+
244
+ Todas exigem `Authorization: Bearer <token>` (enviado automaticamente). Ao criar uma
245
+ notificação, o servidor persiste o registro **e** dispara o push via FCM para os devices
246
+ do destinatário.
162
247
 
163
248
  ---
164
249
 
@@ -3,6 +3,7 @@ import 'package:kasy_kit/core/data/api/http_client.dart';
3
3
  import 'package:kasy_kit/core/data/entities/user_entity.dart';
4
4
  import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
5
5
  import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
6
+ import 'package:flutter/foundation.dart' show kIsWeb;
6
7
  import 'package:flutter/services.dart' show PlatformException;
7
8
  import 'package:dio/dio.dart';
8
9
  import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
@@ -119,6 +120,15 @@ class HttpAuthenticationApi implements AuthenticationApi {
119
120
 
120
121
  @override
121
122
  Future<Credentials> signinWithGoogle() async {
123
+ // google_sign_in v7 authenticate() is unsupported on web (throws). The API
124
+ // backend is wire-it-yourself, so fail clearly instead of an opaque crash.
125
+ if (kIsWeb) {
126
+ throw UnimplementedError(
127
+ 'Google sign-in on web is not wired for the API backend. Use '
128
+ 'google_sign_in_web (renderButton) or a backend OAuth redirect, then '
129
+ 'exchange the token with your API.',
130
+ );
131
+ }
122
132
  final googleSignIn = GoogleSignIn.instance;
123
133
  await googleSignIn.initialize(
124
134
  clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'),
@@ -153,6 +163,15 @@ class HttpAuthenticationApi implements AuthenticationApi {
153
163
 
154
164
  @override
155
165
  Future<Credentials> signinWithApple() async {
166
+ // Apple on web needs a paid Apple Service ID + secret and a backend token
167
+ // exchange; getAppleIDCredential crashes on web. The UI hides the Apple button
168
+ // on web; guard here too so a programmatic call fails clearly.
169
+ if (kIsWeb) {
170
+ throw UnimplementedError(
171
+ 'Apple sign-in on web needs a paid Apple Service ID + secret and a backend '
172
+ 'token exchange; it is not wired for the API backend.',
173
+ );
174
+ }
156
175
  late final AuthorizationCredentialAppleID credential;
157
176
  try {
158
177
  credential = await SignInWithApple.getAppleIDCredential(
@@ -212,6 +231,14 @@ class HttpAuthenticationApi implements AuthenticationApi {
212
231
  /// POST /auth/link-google {id_token, access_token} → Credentials
213
232
  @override
214
233
  Future<Credentials> signupFromAnonymousWithGoogle() async {
234
+ // See signinWithGoogle: authenticate() is unsupported on web.
235
+ if (kIsWeb) {
236
+ throw UnimplementedError(
237
+ 'Google sign-in on web is not wired for the API backend. Use '
238
+ 'google_sign_in_web (renderButton) or a backend OAuth redirect, then '
239
+ 'exchange the token with your API.',
240
+ );
241
+ }
215
242
  final googleSignIn = GoogleSignIn.instance;
216
243
  await googleSignIn.initialize(
217
244
  clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'),
@@ -242,6 +269,13 @@ class HttpAuthenticationApi implements AuthenticationApi {
242
269
  /// POST /auth/link-apple {identity_token, authorization_code} → Credentials
243
270
  @override
244
271
  Future<Credentials> signupFromAnonymousWithApple() async {
272
+ // See signinWithApple: Apple on web is not wired for the API backend.
273
+ if (kIsWeb) {
274
+ throw UnimplementedError(
275
+ 'Apple sign-in on web needs a paid Apple Service ID + secret and a backend '
276
+ 'token exchange; it is not wired for the API backend.',
277
+ );
278
+ }
245
279
  late final AuthorizationCredentialAppleID credential;
246
280
  try {
247
281
  credential = await SignInWithApple.getAppleIDCredential(
@@ -38,6 +38,7 @@ class StripeBackendApi {
38
38
  String? successUrl,
39
39
  String? cancelUrl,
40
40
  String? locale,
41
+ bool? allowPromoCodes,
41
42
  }) async {
42
43
  final Response res = await _client.post(
43
44
  '/stripe/checkout-session',
@@ -46,16 +47,25 @@ class StripeBackendApi {
46
47
  if (successUrl != null) 'successUrl': successUrl,
47
48
  if (cancelUrl != null) 'cancelUrl': cancelUrl,
48
49
  if (locale != null) 'locale': locale,
50
+ if (allowPromoCodes != null) 'allowPromoCodes': allowPromoCodes,
49
51
  },
50
52
  );
51
53
  return (res.data as Map)['url'] as String;
52
54
  }
53
55
 
54
56
  /// Create a Customer Portal session (manage / cancel) and return its URL.
55
- Future<String> createPortalSession({String? returnUrl}) async {
57
+ /// Pass [planSwitching] = true to auto-configure the portal with
58
+ /// upgrade/downgrade support (no manual Stripe dashboard setup needed).
59
+ Future<String> createPortalSession({
60
+ String? returnUrl,
61
+ bool? planSwitching,
62
+ }) async {
56
63
  final Response res = await _client.post(
57
64
  '/stripe/portal-session',
58
- data: {if (returnUrl != null) 'returnUrl': returnUrl},
65
+ data: {
66
+ if (returnUrl != null) 'returnUrl': returnUrl,
67
+ if (planSwitching != null) 'planSwitching': planSwitching,
68
+ },
59
69
  );
60
70
  return (res.data as Map)['url'] as String;
61
71
  }
@@ -743,6 +743,189 @@ async function ensureLocalhostAuthorizedDomains(projectId, token) {
743
743
  return { ok: true, added: missing };
744
744
  }
745
745
 
746
+ /**
747
+ * Authorize localhost (and 127.0.0.1) on a project's Firebase Auth config,
748
+ * fetching the gcloud access token internally. Exported so callers that don't
749
+ * run the full setup (e.g. the Supabase backend, which sets up Firebase in
750
+ * fcmOnly mode and never reaches enableAuthProviders) can still authorize the
751
+ * domains the web Google popup needs. Best-effort: returns { ok, error } and
752
+ * never throws.
753
+ *
754
+ * @param {string} projectId
755
+ * @returns {{ ok: boolean, added?: string[], error?: string }}
756
+ */
757
+ async function authorizeLocalhostForProject(projectId) {
758
+ let token;
759
+ try {
760
+ token = await getAccessToken();
761
+ } catch (_) {
762
+ return { ok: false, error: 'Could not get access token' };
763
+ }
764
+ return ensureLocalhostAuthorizedDomains(projectId, token);
765
+ }
766
+
767
+ /**
768
+ * Configure Apple Sign-In on Firebase for the WEB (and the OAuth code flow) by
769
+ * writing the Apple provider's codeFlowConfig (Service ID + Team ID + Key ID +
770
+ * `.p8`) via the Identity Toolkit Admin v2 API. Once stored, Firebase re-signs the
771
+ * short-lived client secret itself, so it never expires.
772
+ *
773
+ * Existing bundleIds (used by the native iOS flow) are preserved and the project's
774
+ * own bundleId is merged in, so configuring web never breaks native.
775
+ *
776
+ * @param {object} opts
777
+ * @param {string} opts.projectId
778
+ * @param {string} opts.serviceId - Apple Service ID (becomes the provider clientId)
779
+ * @param {string} opts.teamId
780
+ * @param {string} opts.keyId
781
+ * @param {string} opts.privateKey - PEM contents of the .p8
782
+ * @param {string} [opts.bundleId] - app bundle id to keep allowed for native sign-in
783
+ * @returns {{ ok: boolean, error?: string }}
784
+ */
785
+ async function configureFirebaseAppleWeb({ projectId, serviceId, teamId, keyId, privateKey, bundleId }) {
786
+ if (!serviceId || !teamId || !keyId || !privateKey) {
787
+ return { ok: false, error: 'serviceId, teamId, keyId and privateKey are required' };
788
+ }
789
+ let token;
790
+ try {
791
+ token = await getAccessToken();
792
+ } catch (_) {
793
+ return { ok: false, error: 'Could not get access token' };
794
+ }
795
+
796
+ const headers = {
797
+ Authorization: `Bearer ${token}`,
798
+ 'Content-Type': 'application/json',
799
+ 'X-Goog-User-Project': projectId,
800
+ };
801
+ const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
802
+
803
+ // Read the current Apple config (if any) so we preserve the native bundleIds.
804
+ let existing = null;
805
+ try {
806
+ const getRes = await fetch(`${base}/apple.com`, { headers });
807
+ if (getRes.ok) existing = await getRes.json();
808
+ } catch (_) {
809
+ // No existing config (or transient) — we'll create it below.
810
+ }
811
+
812
+ const bundleIds = Array.from(
813
+ new Set([...(existing?.appleSignInConfig?.bundleIds || []), bundleId].filter(Boolean)),
814
+ );
815
+
816
+ const body = {
817
+ name: `projects/${projectId}/defaultSupportedIdpConfigs/apple.com`,
818
+ enabled: true,
819
+ clientId: serviceId,
820
+ appleSignInConfig: {
821
+ codeFlowConfig: { teamId, keyId, privateKey },
822
+ ...(bundleIds.length ? { bundleIds } : {}),
823
+ },
824
+ };
825
+
826
+ try {
827
+ if (existing) {
828
+ const res = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
829
+ method: 'PATCH',
830
+ headers,
831
+ body: JSON.stringify(body),
832
+ });
833
+ if (!res.ok) {
834
+ const text = await res.text();
835
+ return { ok: false, error: `PATCH failed (${res.status}): ${text.slice(0, 200)}` };
836
+ }
837
+ } else {
838
+ const res = await fetch(`${base}?idpId=apple.com`, {
839
+ method: 'POST',
840
+ headers,
841
+ body: JSON.stringify(body),
842
+ });
843
+ if (!res.ok) {
844
+ const text = await res.text();
845
+ // Raced with another writer — fall back to PATCH.
846
+ if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
847
+ const patchRes = await fetch(`${base}/apple.com?updateMask=enabled,clientId,appleSignInConfig`, {
848
+ method: 'PATCH',
849
+ headers,
850
+ body: JSON.stringify(body),
851
+ });
852
+ if (!patchRes.ok) {
853
+ const t2 = await patchRes.text();
854
+ return { ok: false, error: `PATCH failed (${patchRes.status}): ${t2.slice(0, 200)}` };
855
+ }
856
+ } else {
857
+ return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
858
+ }
859
+ }
860
+ }
861
+ } catch (err) {
862
+ return { ok: false, error: `Network error: ${err.message}` };
863
+ }
864
+
865
+ return { ok: true };
866
+ }
867
+
868
+ /**
869
+ * Enable the Facebook provider on Firebase via the Identity Toolkit Admin v2 API.
870
+ * Needs the Meta App ID (clientId) + App Secret (clientSecret). The native App ID /
871
+ * Client Token live in Info.plist / strings.xml and are written separately.
872
+ *
873
+ * @param {object} opts - { projectId, appId, appSecret }
874
+ * @returns {{ ok: boolean, error?: string }}
875
+ */
876
+ async function configureFirebaseFacebook({ projectId, appId, appSecret }) {
877
+ if (!appId || !appSecret) {
878
+ return { ok: false, error: 'appId and appSecret are required' };
879
+ }
880
+ let token;
881
+ try {
882
+ token = await getAccessToken();
883
+ } catch (_) {
884
+ return { ok: false, error: 'Could not get access token' };
885
+ }
886
+
887
+ const headers = {
888
+ Authorization: `Bearer ${token}`,
889
+ 'Content-Type': 'application/json',
890
+ 'X-Goog-User-Project': projectId,
891
+ };
892
+ const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/defaultSupportedIdpConfigs`;
893
+ const body = {
894
+ name: `projects/${projectId}/defaultSupportedIdpConfigs/facebook.com`,
895
+ enabled: true,
896
+ clientId: appId,
897
+ clientSecret: appSecret,
898
+ };
899
+
900
+ try {
901
+ let res = await fetch(`${base}?idpId=facebook.com`, {
902
+ method: 'POST',
903
+ headers,
904
+ body: JSON.stringify(body),
905
+ });
906
+ if (!res.ok) {
907
+ const text = await res.text();
908
+ if (res.status === 409 || text.includes('ALREADY_EXISTS')) {
909
+ res = await fetch(`${base}/facebook.com?updateMask=enabled,clientId,clientSecret`, {
910
+ method: 'PATCH',
911
+ headers,
912
+ body: JSON.stringify(body),
913
+ });
914
+ if (!res.ok) {
915
+ const t2 = await res.text();
916
+ return { ok: false, error: `PATCH failed (${res.status}): ${t2.slice(0, 200)}` };
917
+ }
918
+ } else {
919
+ return { ok: false, error: `POST failed (${res.status}): ${text.slice(0, 200)}` };
920
+ }
921
+ }
922
+ } catch (err) {
923
+ return { ok: false, error: `Network error: ${err.message}` };
924
+ }
925
+
926
+ return { ok: true };
927
+ }
928
+
746
929
  /**
747
930
  * Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
748
931
  * have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
@@ -1277,6 +1460,9 @@ module.exports = {
1277
1460
  enableAuthProviders,
1278
1461
  ensureFirebaseAuthInitialized,
1279
1462
  ensureLocalhostAuthorizedDomains,
1463
+ authorizeLocalhostForProject,
1464
+ configureFirebaseAppleWeb,
1465
+ configureFirebaseFacebook,
1280
1466
  listBillingAccounts,
1281
1467
  listGcpOrganizations,
1282
1468
  checkGcloudAuth,
@@ -15,6 +15,7 @@ const path = require('node:path');
15
15
  const os = require('node:os');
16
16
  const fs = require('fs-extra');
17
17
  const { augmentedEnv } = require('../../../utils/env-tools');
18
+ const { signAppleClientSecret } = require('../../../utils/apple-web');
18
19
 
19
20
  const execAsync = promisify(exec);
20
21
 
@@ -407,6 +408,95 @@ async function enableAppleSignIn(projectRef, bundleId) {
407
408
  return { ok: false, error: result.data.message || JSON.stringify(result.data) };
408
409
  }
409
410
 
411
+ /**
412
+ * GET the current Supabase auth config (read-only). Used to merge values we must
413
+ * not clobber (e.g. the native bundle id already in external_apple_client_id).
414
+ */
415
+ async function getSupabaseAuthConfig(projectRef, token) {
416
+ try {
417
+ const res = await fetch(`https://api.supabase.com/v1/projects/${projectRef}/config/auth`, {
418
+ headers: { Authorization: `Bearer ${token}` },
419
+ });
420
+ if (!res.ok) return null;
421
+ return await res.json();
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Enable Apple Sign-In on the WEB for Supabase.
429
+ *
430
+ * Unlike Firebase (which stores the .p8 and re-signs), Supabase stores a static,
431
+ * pre-signed client secret JWT that expires every ~6 months. We sign it here with
432
+ * the developer's `.p8` and write it as external_apple_secret. The Service ID is
433
+ * added to external_apple_client_id (the audience list) alongside the native bundle
434
+ * id, so both native iOS and the web OAuth flow validate.
435
+ *
436
+ * @param {string} projectRef
437
+ * @param {object} opts - { serviceId, teamId, keyId, privateKey, bundleId? }
438
+ * @returns {{ ok: boolean, error?: string, expiresAt?: number }}
439
+ */
440
+ async function enableAppleWebSignIn(projectRef, { serviceId, teamId, keyId, privateKey, bundleId } = {}) {
441
+ if (!serviceId || !teamId || !keyId || !privateKey) {
442
+ return { ok: false, error: 'serviceId, teamId, keyId and privateKey are required' };
443
+ }
444
+ const token = await getSupabaseAccessToken();
445
+ if (!token) return { ok: false, error: 'Could not retrieve Supabase access token' };
446
+
447
+ let secret;
448
+ let expiresAt;
449
+ try {
450
+ ({ token: secret, expiresAt } = signAppleClientSecret({ serviceId, teamId, keyId, privateKey }));
451
+ } catch (err) {
452
+ return { ok: false, error: err.message };
453
+ }
454
+
455
+ // Merge the Service ID into the existing audience list without dropping the
456
+ // native bundle id the CLI set at creation.
457
+ const current = await getSupabaseAuthConfig(projectRef, token);
458
+ const existingIds = String(current?.external_apple_client_id || '')
459
+ .split(',')
460
+ .map((s) => s.trim())
461
+ .filter(Boolean);
462
+ const clientIds = Array.from(new Set([...existingIds, bundleId, serviceId].filter(Boolean))).join(',');
463
+
464
+ const result = await patchAuthConfig(projectRef, token, {
465
+ external_apple_enabled: true,
466
+ external_apple_client_id: clientIds,
467
+ external_apple_secret: secret,
468
+ });
469
+ if (!result.ok) return { ok: false, error: result.error };
470
+ if (result.data.external_apple_enabled === true) return { ok: true, expiresAt };
471
+ return { ok: false, error: result.data.message || JSON.stringify(result.data) };
472
+ }
473
+
474
+ /**
475
+ * Enable the Facebook provider on Supabase via the Management API.
476
+ * Needs the Meta App ID (client_id) + App Secret (secret). The native App ID /
477
+ * Client Token live in Info.plist / strings.xml and are written separately.
478
+ *
479
+ * @param {string} projectRef
480
+ * @param {object} opts - { appId, appSecret }
481
+ * @returns {{ ok: boolean, error?: string }}
482
+ */
483
+ async function enableFacebookSignIn(projectRef, { appId, appSecret } = {}) {
484
+ if (!appId || !appSecret) {
485
+ return { ok: false, error: 'appId and appSecret are required' };
486
+ }
487
+ const token = await getSupabaseAccessToken();
488
+ if (!token) return { ok: false, error: 'Could not retrieve Supabase access token' };
489
+
490
+ const result = await patchAuthConfig(projectRef, token, {
491
+ external_facebook_enabled: true,
492
+ external_facebook_client_id: appId,
493
+ external_facebook_secret: appSecret,
494
+ });
495
+ if (!result.ok) return { ok: false, error: result.error };
496
+ if (result.data.external_facebook_enabled === true) return { ok: true };
497
+ return { ok: false, error: result.data.message || JSON.stringify(result.data) };
498
+ }
499
+
410
500
  /**
411
501
  * Configure auth settings via Supabase Management API:
412
502
  * - Enable anonymous sign-in
@@ -717,6 +807,8 @@ module.exports = {
717
807
  enableAnonymousSignIn,
718
808
  enableGoogleSignIn,
719
809
  enableAppleSignIn,
810
+ enableAppleWebSignIn,
811
+ enableFacebookSignIn,
720
812
  checkLoggedIn,
721
813
  getOrgsList,
722
814
  getProjectsByOrg,