kasy-cli 1.19.3 → 1.20.1

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 (83) hide show
  1. package/README.md +11 -3
  2. package/bin/kasy.js +1 -0
  3. package/lib/commands/new.js +87 -37
  4. package/lib/commands/run.js +14 -0
  5. package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
  6. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  7. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
  9. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
  10. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
  11. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
  12. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
  14. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  17. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
  18. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
  19. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
  20. package/lib/scaffold/backends/firebase/setup-from-scratch.js +91 -2
  21. package/lib/scaffold/backends/supabase/deploy.js +56 -3
  22. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
  23. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
  24. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
  25. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  26. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  27. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
  28. package/lib/scaffold/catalog.js +2 -2
  29. package/lib/scaffold/engine.js +5 -0
  30. package/lib/scaffold/generate.js +23 -3
  31. package/lib/scaffold/shared/generator-utils.js +303 -56
  32. package/lib/scaffold/shared/post-build.js +11 -0
  33. package/lib/utils/i18n/messages-en.js +6 -1
  34. package/lib/utils/i18n/messages-es.js +6 -1
  35. package/lib/utils/i18n/messages-pt.js +6 -1
  36. package/package.json +1 -1
  37. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  42. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  43. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  44. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  45. package/templates/firebase/lib/components/kasy_date_picker.dart +14 -8
  46. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +1150 -0
  47. package/templates/firebase/lib/components/kasy_tabs.dart +156 -43
  48. package/templates/firebase/lib/components/kasy_text_field.dart +37 -34
  49. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +13 -82
  50. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -102
  51. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +8 -1
  52. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +433 -243
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +198 -83
  54. package/templates/firebase/lib/core/icons/kasy_icons.dart +1 -0
  55. package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
  56. package/templates/firebase/lib/core/theme/colors.dart +6 -2
  57. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +119 -19
  58. package/templates/firebase/lib/core/widgets/kasy_hover.dart +68 -27
  59. package/templates/firebase/lib/features/home/home_components_page.dart +11 -14
  60. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +121 -66
  61. package/templates/firebase/lib/features/home/home_page.dart +7 -8
  62. package/templates/firebase/lib/features/settings/settings_page.dart +27 -146
  63. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +16 -3
  64. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +22 -5
  65. package/templates/firebase/lib/i18n/en.i18n.json +3 -1
  66. package/templates/firebase/lib/i18n/es.i18n.json +3 -1
  67. package/templates/firebase/lib/i18n/pt.i18n.json +3 -1
  68. package/templates/firebase/lib/router.dart +60 -0
  69. package/templates/firebase/pubspec.yaml +6 -4
  70. package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
  71. package/templates/firebase/web/index.html +7 -17
  72. package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  73. package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
  74. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  75. package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
  76. package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
  77. package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  78. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  79. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
  80. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  81. package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
  82. package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
  83. package/templates/firebase/lib/firebase_options.dart +0 -75
package/README.md CHANGED
@@ -4,14 +4,22 @@ CLI for scaffolding production-ready Flutter SaaS apps.
4
4
 
5
5
  ## Install
6
6
 
7
+ **macOS & Linux**
8
+
7
9
  ```bash
8
- npm install -g kasy-cli
10
+ curl -fsSL https://kasy.dev/install | bash
11
+ ```
12
+
13
+ **Windows (PowerShell)**
14
+
15
+ ```powershell
16
+ irm https://kasy.dev/install.ps1 | iex
9
17
  ```
10
18
 
11
- Or run without installing:
19
+ Or via npm:
12
20
 
13
21
  ```bash
14
- npx kasy-cli new
22
+ npm install -g kasy-cli
15
23
  ```
16
24
 
17
25
  ## Quick start
package/bin/kasy.js CHANGED
@@ -357,6 +357,7 @@ function buildProgram(language) {
357
357
  .option('--web', 'Run on web — prints localhost URL in lime so you open it in your own browser (your extensions, your accounts)')
358
358
  .option('--open', 'With --web: auto-launch a clean Chrome window (Flutter profile, no extensions) instead of just printing the URL')
359
359
  .option('--web-port <port>', 'Fixed port for web (default 5555) — keeps the origin stable so Firebase Auth sessions persist between runs')
360
+ .option('--web-hostname <host>', 'Host for web (default localhost) — localhost is a Firebase-authorized domain by default, so Google sign-in works without console changes')
360
361
  .option('-d, --device <id>', 'Run on specific device ID')
361
362
  .option('--prod', 'Use production dart-defines (from launch.json)')
362
363
  .option('--no-defines', 'Skip dart-defines from launch.json')
@@ -50,7 +50,7 @@ const { generateFirebaseProject } = require('../scaffold/backends/firebase/gener
50
50
  const { generateSupabaseProject } = require('../scaffold/backends/supabase/generator');
51
51
  const { generateApiProject } = require('../scaffold/backends/api/generator');
52
52
  const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys } = require('../scaffold/backends/supabase/deploy');
53
- const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme } = require('../scaffold/shared/post-build');
53
+ const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
54
54
  const { toPackageName } = require('../scaffold/backends/firebase/tokens');
55
55
  const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getGcloudInstallInstructions, enableAuthProviders, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
56
56
  const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
@@ -212,7 +212,7 @@ const STEP_LABELS = {
212
212
  'supabase db push': { en: 'Database migrated', pt: 'Banco migrado', es: 'Base de datos migrada' },
213
213
  'anonymous sign-in': { en: 'Anonymous sign-in enabled', pt: 'Login anonimo ativado', es: 'Inicio de sesion anonimo activado' },
214
214
  'google sign-in': { en: 'Google Sign-In enabled', pt: 'Google Sign-In ativado', es: 'Google Sign-In activado' },
215
- 'apple sign-in': { en: 'Apple Sign-In ready (configure credentials before testing kasy.dev/docs/apple)', pt: 'Apple Sign-In preparado (configure as credenciais antes de testar kasy.dev/docs/apple)', es: 'Apple Sign-In listo (configura las credenciales antes de probar kasy.dev/docs/apple)' },
215
+ 'apple sign-in': { en: 'Apple Sign-In enabled (native iOS ready; for web add a Service ID at kasy.dev/docs/apple)', pt: 'Apple Sign-In ativado (iOS nativo pronto; para web adicione um Service ID em kasy.dev/docs/apple)', es: 'Apple Sign-In activado (iOS nativo listo; para web añade un Service ID en kasy.dev/docs/apple)' },
216
216
  'google-auth-options': { en: 'Google client IDs written', pt: 'Client IDs do Google gravados', es: 'Client IDs de Google escritos' },
217
217
  'ios-url-scheme': { en: 'iOS URL scheme registered', pt: 'URL scheme iOS registrado', es: 'URL scheme iOS registrado' },
218
218
  'google-ios-url-scheme': { en: 'iOS Google URL scheme registered', pt: 'URL scheme Google iOS registrado', es: 'URL scheme Google iOS registrado' },
@@ -220,7 +220,7 @@ const STEP_LABELS = {
220
220
  'secret REVENUECAT_WEBHOOK_KEY': { en: 'RevenueCat webhook secret', pt: 'Secret webhook RevenueCat', es: 'Secret webhook RevenueCat' },
221
221
  'secret META_ACCESS_TOKEN': { en: 'Meta Access Token', pt: 'Meta Access Token', es: 'Meta Access Token' },
222
222
  'secret META_DATASET_ID': { en: 'Meta Dataset ID', pt: 'Meta Dataset ID', es: 'Meta Dataset ID' },
223
- 'fcm-key': { en: 'FCM Service Account key generated', pt: 'Chave FCM gerada automaticamente', es: 'Clave FCM generada automáticamente' },
223
+ 'fcm-key': { en: 'FCM Service Account key', pt: 'Chave de Service Account FCM', es: 'Clave de Service Account FCM' },
224
224
  'fcm-key-saved': { en: 'FCM key saved to .kasy/', pt: 'Chave FCM salva em .kasy/', es: 'Clave FCM guardada en .kasy/' },
225
225
  'secret FIREBASE_SERVICE_ACCOUNT_JSON': { en: 'FCM Service Account configured', pt: 'Service Account FCM configurado', es: 'Service Account FCM configurado' },
226
226
  'gcp-project': { en: 'GCP project created', pt: 'Projeto GCP criado', es: 'Proyecto GCP creado' },
@@ -233,6 +233,7 @@ const STEP_LABELS = {
233
233
  'sha1': { en: 'SHA-1 added for Google Sign-In', pt: 'SHA-1 adicionado para Google Sign-In', es: 'SHA-1 añadido para Google Sign-In' },
234
234
  'service-account': { en: 'Service account key created', pt: 'Chave de conta de servico criada', es: 'Clave de cuenta de servicio creada' },
235
235
  'firestore': { en: 'Firestore database created', pt: 'Banco Firestore criado', es: 'Base de datos Firestore creada' },
236
+ 'firestore-rules': { en: 'Firestore security rules deployed', pt: 'Regras de seguranca Firestore publicadas', es: 'Reglas de seguridad Firestore desplegadas' },
236
237
  'storage': { en: 'Firebase Storage bucket created', pt: 'Bucket Firebase Storage criado', es: 'Bucket Firebase Storage creado' },
237
238
  'storage-cors': { en: 'CORS enabled on Storage (web images)', pt: 'CORS ativado no Storage (imagens na web)', es: 'CORS activado en Storage (imágenes en web)' },
238
239
  };
@@ -255,6 +256,7 @@ const STEP_PROGRESS = {
255
256
  'sha1': { en: 'Adding SHA-1 for Google Sign-In…', pt: 'Adicionando SHA-1 para Google Sign-In…', es: 'Añadiendo SHA-1 para Google Sign-In…' },
256
257
  'service-account': { en: 'Creating service account key…', pt: 'Criando chave de conta de servico…', es: 'Creando clave de cuenta de servicio…' },
257
258
  'firestore': { en: 'Creating Firestore database…', pt: 'Criando banco Firestore…', es: 'Creando base de datos Firestore…' },
259
+ 'firestore-rules': { en: 'Deploying Firestore security rules…', pt: 'Publicando regras de seguranca Firestore…', es: 'Desplegando reglas de seguridad Firestore…' },
258
260
  'storage': { en: 'Creating Firebase Storage bucket…', pt: 'Criando bucket Firebase Storage…', es: 'Creando bucket Firebase Storage…' },
259
261
  'storage-cors': { en: 'Enabling CORS on Storage…', pt: 'Ativando CORS no Storage…', es: 'Activando CORS en Storage…' },
260
262
  'deploy-retry-wait': { en: 'Waiting 4 min for GCP permissions to propagate… (do not close terminal)', pt: 'Aguardando 4 min para permissões do GCP propagarem… (não feche o terminal)', es: 'Esperando 4 min para propagar permisos de GCP… (no cierres la terminal)' },
@@ -772,6 +774,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
772
774
  } else if (key === 'auth-providers-warn') {
773
775
  ps1.stop();
774
776
  ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
777
+ } else if (key === 'auth-localhost-warn') {
778
+ ps1.stop();
779
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(data?.url || '')}`);
775
780
  }
776
781
  };
777
782
  let setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
@@ -1179,6 +1184,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1179
1184
  } else if (key === 'auth-google-warn') {
1180
1185
  ps4.stop();
1181
1186
  ui.log.warn(`${tr('new.firebase.existing.googleSignInManual')}\n${kleur.cyan(data?.url || '')}`);
1187
+ } else if (key === 'auth-localhost-warn') {
1188
+ ps4.stop();
1189
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(data?.url || '')}`);
1182
1190
  }
1183
1191
  },
1184
1192
  });
@@ -1209,16 +1217,23 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1209
1217
  // --with flag was passed: use those modules directly, skip preset prompt.
1210
1218
  modules = preselectedModules;
1211
1219
  } else if (isQuick) {
1212
- // Quick mode: ship all features by default. Facebook is excluded because
1213
- // it requires App ID + token that we can't auto-generate — the user adds
1214
- // it later with `kasy add facebook` when they have the credentials.
1215
- modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook');
1220
+ // Quick mode: ship all features the backend supports. Facebook is excluded
1221
+ // because it requires App ID + token that we can't auto-generate — the user
1222
+ // adds it later with `kasy add facebook` when they have the credentials.
1223
+ // Filter by availableIn so backend-specific features (e.g. feedback needs a
1224
+ // DB) don't leak into a backend that can't support them.
1225
+ const quickAvailable = new Set(
1226
+ getVisibleFeatures({ audience: KASY_AUDIENCE, backend }).map((f) => f.id)
1227
+ );
1228
+ modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook' && quickAvailable.has(m));
1216
1229
  } else {
1217
1230
  section('new.advanced.section.features');
1218
1231
  // Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
1219
1232
  const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
1220
1233
 
1221
- // web has an extra runtime constraint: firebase create-mode only
1234
+ // In Firebase create-mode the web app is already decided by the
1235
+ // `firebaseIncludeWeb` prompt above, so hide `web` from the multiselect there
1236
+ // to avoid asking twice. Supabase, API and Firebase existing-mode show it.
1222
1237
  const isWebExcluded = backend === 'firebase' && firebaseSetupMode === 'create';
1223
1238
 
1224
1239
  // Visual groups in display order (header key → feature ids in this group)
@@ -1473,7 +1488,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1473
1488
  firebaseProjectId: core.firebaseProjectId.trim(),
1474
1489
  modules: modules || [],
1475
1490
  backend,
1476
- includeWeb: backend === 'firebase' ? firebaseIncludeWeb : true,
1491
+ // Web platform follows the `web` feature so the flutterfire web app and the
1492
+ // web/ folder stay in sync across all backends (Firebase, Supabase, API).
1493
+ includeWeb: modules.includes('web'),
1477
1494
  supabaseUrl: core.supabaseUrl?.trim(),
1478
1495
  supabaseAnonKey: core.supabaseAnonKey?.trim(),
1479
1496
  apiBaseUrl: core.apiBaseUrl?.trim(),
@@ -1581,38 +1598,66 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1581
1598
  : (supabaseExistingResult?.ok ? { projectRef: supabaseExistingResult.projectRef, dbPassword: supabaseExistingResult.dbPassword } : null);
1582
1599
 
1583
1600
  if (backend === 'supabase') {
1584
- // ── Auto-resolve Google credentials from flutterfire output files ────────
1601
+ // ── Google Sign-In: create a real Google OAuth client, then push it to Supabase ──
1585
1602
  const flutterfireOk = result.steps.find((s) => s.name === 'flutterfire')?.ok;
1586
- if (flutterfireOk) {
1603
+ if (flutterfireOk && answers.firebaseProjectId) {
1604
+ // Supabase Google Sign-In needs a genuine OAuth Web Client (id + secret).
1605
+ // The only reliable way to create it is the Firebase CLI auth deploy, the
1606
+ // same path the Firebase backend uses. The REST API cannot create the OAuth
1607
+ // client, which is why Google used to come out disabled on Supabase projects.
1608
+ const googleSpinner = ui.spinner();
1609
+ googleSpinner.start(tr('new.google.enabling'));
1610
+ const cliResult = await enableAuthViaFirebaseCli({
1611
+ projectDir: targetDir,
1612
+ projectId: answers.firebaseProjectId,
1613
+ appName: answers.appName,
1614
+ });
1615
+ googleSpinner.stop(tr('new.google.enabling'));
1616
+
1617
+ if (cliResult.ok) {
1618
+ // The deploy created the OAuth Web Client + iOS client. Re-run flutterfire so
1619
+ // google-services.json and GoogleService-Info.plist pick up the new Client IDs.
1620
+ const rerunSpinner = ui.spinner();
1621
+ rerunSpinner.start(tr('new.google.refreshConfigs'));
1622
+ await flutterfireConfigure(targetDir, answers.firebaseProjectId, {
1623
+ includeWeb: answers.includeWeb !== false,
1624
+ });
1625
+ rerunSpinner.stop(tr('new.google.refreshConfigs'));
1626
+ } else if (cliResult.error === 'support_email_required') {
1627
+ ui.log.warn(tr('new.google.manualHint.noEmail'));
1628
+ }
1629
+
1630
+ // Read the Web + iOS Client IDs from the refreshed config files.
1587
1631
  const fileCreds = await readSupabaseGoogleCredentials(targetDir);
1588
1632
  googleWebClientId = fileCreds.webClientId;
1589
1633
  googleIosClientId = fileCreds.iosClientId;
1590
1634
 
1591
- // Enable Google Sign-In in Firebase Auth so Identity Toolkit stores the client secret.
1592
- // This is the same step that setupFromScratch runs for Firebase backend.
1593
- // For Supabase, Firebase Auth is not used for auth, but enabling Google here is the
1594
- // only way to make the client secret available via the Identity Toolkit API.
1595
- // Non-fatal: silently ignored if it fails (e.g. API not yet propagated).
1596
- if (answers.firebaseProjectId) {
1597
- const authResult = await enableAuthProviders(answers.firebaseProjectId);
1598
- if (authResult.ok && !authResult.googleSignInSkipped) {
1599
- printStepResult({ name: 'google sign-in', ok: true }, language);
1600
- }
1601
- if (authResult.appleEnabled) {
1602
- printStepResult({ name: 'apple sign-in', ok: true }, language);
1635
+ // Read the OAuth Client Secret from the Identity Toolkit now that the client
1636
+ // exists. It can lag a moment behind the deploy, so retry briefly.
1637
+ if (googleWebClientId) {
1638
+ for (let attempt = 1; attempt <= 3 && !googleClientSecret; attempt++) {
1639
+ googleClientSecret = await getGoogleClientSecretViaGcloud(answers.firebaseProjectId);
1640
+ if (!googleClientSecret && attempt < 3) {
1641
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1642
+ }
1603
1643
  }
1604
1644
  }
1605
1645
 
1606
- // Try to get the Client Secret from Identity Toolkit API (requires gcloud auth)
1607
- if (answers.firebaseProjectId && googleWebClientId) {
1608
- googleClientSecret = await getGoogleClientSecretViaGcloud(answers.firebaseProjectId);
1609
- }
1610
-
1611
- // Always write google_auth_options.dart with whatever credentials we have
1646
+ // Persist the Client IDs into the app and register the iOS URL scheme.
1612
1647
  if (googleWebClientId) {
1613
1648
  const gaResult = await writeSupabaseGoogleAuthOptions(targetDir, googleWebClientId, googleIosClientId);
1614
1649
  printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
1615
1650
  }
1651
+ if (googleIosClientId) {
1652
+ const iosResult = await writeGoogleIosUrlSchemeFromClientId(targetDir, googleIosClientId);
1653
+ printStepResult({ name: 'google-ios-url-scheme', ok: iosResult.ok, detail: iosResult.error }, language);
1654
+ }
1655
+
1656
+ // Google is only enabled on Supabase when we have both the Web Client ID and
1657
+ // its secret. If either is missing, point the user to the dashboard to finish.
1658
+ if (!googleWebClientId || !googleClientSecret) {
1659
+ ui.log.warn(tr('new.google.supabaseManual'));
1660
+ }
1616
1661
  }
1617
1662
 
1618
1663
  if (supabaseSetupPayload) {
@@ -1625,9 +1670,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1625
1670
  fcmSpinner.stop(tr('new.fcm.generating'));
1626
1671
  if (fcmResult.ok) {
1627
1672
  fcmServiceAccountJson = fcmResult.json;
1628
- printStepResult({ name: 'fcm-key', ok: true }, language);
1673
+ printStepResult({ name: 'fcm-key', ok: true, detail: tr('new.fcm.ok') }, language);
1629
1674
  } else {
1630
- printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
1675
+ printStepResult({ name: 'fcm-key', ok: false, detail: tr('new.fcm.failSupabase') }, language);
1631
1676
  }
1632
1677
  }
1633
1678
 
@@ -1651,12 +1696,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1651
1696
  const google = googleWebClientId && googleClientSecret
1652
1697
  ? { webClientId: googleWebClientId, clientSecret: googleClientSecret }
1653
1698
  : {};
1699
+ const apple = answers.bundleId ? { bundleId: answers.bundleId } : {};
1654
1700
  const setupSteps = await setupLinkedProject(
1655
1701
  targetDir,
1656
1702
  supabaseSetupPayload.projectRef,
1657
1703
  supabaseSetupPayload.dbPassword,
1658
1704
  secrets,
1659
- google
1705
+ google,
1706
+ apple
1660
1707
  );
1661
1708
  setupSpinner.stop(tr('new.supabase.setup'));
1662
1709
  setupSteps.forEach((s) => printStepResult(s, language));
@@ -1718,7 +1765,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1718
1765
  printStepResult({ name: 'fcm-key-saved', ok: false, detail: 'salvar arquivo de chave falhou' }, language);
1719
1766
  }
1720
1767
  } else {
1721
- printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
1768
+ printStepResult({ name: 'fcm-key', ok: false, detail: tr('new.fcm.failApi') }, language);
1722
1769
  }
1723
1770
  }
1724
1771
 
@@ -1759,12 +1806,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1759
1806
  }
1760
1807
 
1761
1808
  // ── Google + Apple Sign-In: use Firebase CLI for Google (creates OAuth client),
1762
- // then REST API for Apple as best-effort. ───────────────────────────────────
1809
+ // then REST API for Apple as best-effort.
1763
1810
  // Firebase CLI's `deploy --only auth` is the only documented path that auto-
1764
- // creates the OAuth 2.0 Web Client without manual Console clicks the same
1811
+ // creates the OAuth 2.0 Web Client without manual Console clicks, the same
1765
1812
  // backend that the Console hits internally.
1766
- // (Supabase backend keeps the REST-only retry its OAuth client is provisioned
1767
- // differently via the linked Firebase Web app.)
1813
+ // (The Supabase backend runs the same CLI deploy earlier, in its own block, then
1814
+ // pushes the resulting Google + Apple credentials to Supabase via the Mgmt API.)
1768
1815
  if (backend === 'firebase' && answers.firebaseProjectId && flutterfireStep?.ok) {
1769
1816
  const googleSpinner = ui.spinner();
1770
1817
  googleSpinner.start(tr('new.google.enabling'));
@@ -1804,6 +1851,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1804
1851
  if (appleResult.appleEnabled) {
1805
1852
  printStepResult({ name: 'apple sign-in', ok: true }, language);
1806
1853
  }
1854
+ if (appleResult.localhostAuthorized === false) {
1855
+ ui.log.warn(`${tr('new.firebase.localhostWarn')}\n${kleur.cyan(`https://console.firebase.google.com/project/${answers.firebaseProjectId}/authentication/settings`)}`);
1856
+ }
1807
1857
  }
1808
1858
 
1809
1859
  // APNs key (iOS push) is intentionally not mentioned here — it only becomes
@@ -292,10 +292,24 @@ async function runRun(directory, options = {}) {
292
292
  }
293
293
 
294
294
  if (isChromeTarget || isWebServerTarget) {
295
+ // Pin the host to `localhost` so the app's origin is always an authorized
296
+ // domain. Firebase Auth authorizes `localhost` by default but NOT
297
+ // `127.0.0.1`, and `flutter run -d chrome` would otherwise auto-launch
298
+ // Chrome at 127.0.0.1 — which breaks Google sign-in with
299
+ // [firebase_auth/unauthorized-domain]. Forcing localhost makes Google
300
+ // login work out of the box, no Firebase Console changes required.
301
+ deviceArgs.push('--web-hostname', options.webHostname || 'localhost');
295
302
  // Pin a fixed port so the Chrome origin stays the same between runs.
296
303
  // Firebase Auth persists sessions per-origin (IndexedDB) — a random port
297
304
  // each run means the user gets logged out every restart.
298
305
  deviceArgs.push('--web-port', options.webPort || '5555');
306
+ // Google Sign-In opens an auth popup that must talk back to the app window.
307
+ // The web server's default Cross-Origin-Opener-Policy can sever that link
308
+ // ("Cross-Origin-Opener-Policy policy would block the window.closed call"),
309
+ // so in a plain browser tab (kasy run --web, no dedicated Chrome) the popup
310
+ // can't report success and sign-in appears to fail. `same-origin-allow-popups`
311
+ // keeps the opener relationship intact while staying same-origin safe.
312
+ deviceArgs.push('--web-header', 'Cross-Origin-Opener-Policy=same-origin-allow-popups');
299
313
  }
300
314
 
301
315
  // Read dart-defines from .vscode/launch.json (skip if --no-defines)
@@ -10,7 +10,7 @@ final metaAdsApiProvider = Provider<MetaAdsApi>(
10
10
  ///
11
11
  /// Your backend must expose:
12
12
  /// POST /meta-track-event
13
- /// Authorization: Bearer <user-token>
13
+ /// Authorization: Bearer `<user-token>`
14
14
  /// Body: { "event_name": "CompleteRegistration", "custom_data": {} }
15
15
  ///
16
16
  /// The backend is responsible for calling the Meta Graph API server-to-server.
@@ -34,7 +34,7 @@ abstract class StorageApi {
34
34
  ///
35
35
  /// Expected endpoints:
36
36
  /// POST /storage/upload — multipart/form-data with fields: file, folder, public
37
- /// DELETE /storage/file — JSON body: { "path": "<imagePath>" }
37
+ /// DELETE /storage/file — JSON body: `{ "path": "<imagePath>" }`
38
38
  ///
39
39
  /// Expected upload response JSON:
40
40
  /// { "path": "folder/filename.jpg", "url": "https://..." }
@@ -29,7 +29,7 @@ class Credentials {
29
29
  final String id;
30
30
  // this is the user security token (example: JWT)
31
31
  // but your can use an Oauth2 token with a refresh token too
32
- final String token;
32
+ final String? token;
33
33
 
34
34
  Credentials({
35
35
  required this.id,
@@ -101,7 +101,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
101
101
  }
102
102
 
103
103
  @override
104
- Future<Credentials> signinAnonymously() async {
104
+ Future<Credentials> signinAnonymously() {
105
105
  // REST API backends typically do not support anonymous accounts natively.
106
106
  // Option A – skip anonymous sign-in: remove anonymous auth from the
107
107
  // onboarding flow and require full registration before using the app.
@@ -145,7 +145,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
145
145
  }
146
146
 
147
147
  @override
148
- Future<Credentials> signinWithGooglePlay() async {
148
+ Future<Credentials> signinWithGooglePlay() {
149
149
  // google_sign_in 7.x removed SignInOption.games.
150
150
  // For Play Games support, use the games_services package separately.
151
151
  return signinWithGoogle();
@@ -166,14 +166,13 @@ class HttpAuthenticationApi implements AuthenticationApi {
166
166
  rethrow;
167
167
  }
168
168
  throw UnimplementedError('''
169
- ❌ You must edit lib/features/authentication/api/authentication_api.dart
169
+ ❌ You must edit lib/features/authentication/api/authentication_api.dart
170
170
  to send the Oauth2 token result to your backend
171
+ Available token: ${credential.identityToken}
171
172
  ----------------
172
173
  Please follow the instructions here:
173
174
  https://pub.dev/packages/sign_in_with_apple
174
175
  ''');
175
-
176
-
177
176
  }
178
177
 
179
178
  @override
@@ -11,7 +11,7 @@ final featureRequestApiProvider = Provider<FeatureRequestApi>(
11
11
  );
12
12
 
13
13
  /// REST endpoints expected:
14
- /// GET /feature-requests → List<FeatureRequestEntity> (only active)
14
+ /// GET /feature-requests → `List<FeatureRequestEntity>` (only active)
15
15
  /// POST /feature-requests body: {title, description} → 201 Created
16
16
  /// Server creates with active: false and stores title/description in all locales.
17
17
  class FeatureRequestApi {
@@ -11,7 +11,7 @@ final featureVoteApiProvider = Provider<FeatureVoteApi>(
11
11
  );
12
12
 
13
13
  /// REST endpoints expected:
14
- /// GET /feature-votes → List<UserFeatureVoteEntity> (filtered by authenticated user)
14
+ /// GET /feature-votes → `List<UserFeatureVoteEntity>` (filtered by authenticated user)
15
15
  /// POST /feature-votes → UserFeatureVoteEntity body: {feature_id}
16
16
  /// DELETE /feature-votes/:id → 204 No Content
17
17
  class FeatureVoteApi {
@@ -9,7 +9,7 @@ final llmChatApiProvider = Provider<LlmChatApi>(
9
9
  );
10
10
 
11
11
  /// REST endpoints expected on your backend:
12
- /// GET /llm-messages → List<LlmChatMessageEntity> (current user)
12
+ /// GET /llm-messages → `List<LlmChatMessageEntity>` (current user)
13
13
  /// POST /llm-messages → saves one message
14
14
  /// DELETE /llm-messages → clears all messages (current user)
15
15
  ///