kasy-cli 1.20.0 → 1.21.0-beta.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 (62) hide show
  1. package/README.md +11 -3
  2. package/lib/commands/docs.js +0 -10
  3. package/lib/commands/ios.js +3 -2
  4. package/lib/commands/new.js +98 -58
  5. package/lib/commands/run.js +7 -0
  6. package/lib/scaffold/CHANGELOG.json +14 -0
  7. package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
  8. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  9. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
  11. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
  12. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
  14. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  18. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  19. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
  20. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
  21. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
  22. package/lib/scaffold/backends/firebase/setup-from-scratch.js +10 -8
  23. package/lib/scaffold/backends/supabase/deploy.js +56 -3
  24. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
  25. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
  26. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
  27. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  28. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  29. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
  30. package/lib/scaffold/catalog.js +2 -2
  31. package/lib/scaffold/generate.js +19 -3
  32. package/lib/scaffold/shared/generator-utils.js +265 -55
  33. package/lib/scaffold/shared/post-build.js +22 -6
  34. package/lib/utils/apple-release.js +1 -10
  35. package/lib/utils/browser.js +61 -0
  36. package/lib/utils/checks.js +189 -69
  37. package/lib/utils/env-tools.js +101 -0
  38. package/lib/utils/i18n/messages-en.js +13 -1
  39. package/lib/utils/i18n/messages-es.js +13 -1
  40. package/lib/utils/i18n/messages-pt.js +13 -1
  41. package/package.json +1 -1
  42. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +8 -14
  43. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +38 -128
  44. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -125
  45. package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
  46. package/templates/firebase/lib/core/widgets/kasy_hover.dart +9 -1
  47. package/templates/firebase/lib/features/home/home_components_page.dart +8 -14
  48. package/templates/firebase/lib/features/home/home_page.dart +7 -8
  49. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  50. package/templates/firebase/lib/router.dart +60 -0
  51. package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
  52. package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  53. package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
  54. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  55. package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
  56. package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
  57. package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  58. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  59. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
  60. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  61. package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
  62. package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
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
@@ -1,5 +1,4 @@
1
1
  const path = require('node:path');
2
- const { exec } = require('node:child_process');
3
2
  const fs = require('fs-extra');
4
3
  const kleur = require('kleur');
5
4
  const ui = require('../utils/ui');
@@ -8,15 +7,6 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
8
7
 
9
8
  const DOCS_FILE = path.join(__dirname, '..', '..', 'docs', 'cli-reference.md');
10
9
 
11
- function openInBrowser(url) {
12
- const cmd = process.platform === 'darwin'
13
- ? `open "${url}"`
14
- : process.platform === 'win32'
15
- ? `start "" "${url}"`
16
- : `xdg-open "${url}"`;
17
- exec(cmd);
18
- }
19
-
20
10
  async function runDocs(options = {}) {
21
11
  const t = createTranslator(options.language || detectDefaultLanguage());
22
12
 
@@ -6,6 +6,7 @@ const kleur = require('kleur');
6
6
  const ui = require('../utils/ui');
7
7
  const { printCompactHeader } = require('../utils/brand');
8
8
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
9
+ const { homeDir } = require('../utils/env-tools');
9
10
  const {
10
11
  isKasyFlutterProject,
11
12
  readBundleId,
@@ -73,7 +74,7 @@ async function runConfigure(directory, options = {}) {
73
74
  message: t('ios.configure.q.p8Path'),
74
75
  validate: async (v) => {
75
76
  if (!v || !v.trim()) return t('ios.configure.q.required');
76
- const resolved = path.resolve(v.trim().replace(/^~/, process.env.HOME || ''));
77
+ const resolved = path.resolve(v.trim().replace(/^~/, homeDir()));
77
78
  if (!(await fs.pathExists(resolved))) return t('ios.configure.q.p8NotFound');
78
79
  return undefined;
79
80
  },
@@ -87,7 +88,7 @@ async function runConfigure(directory, options = {}) {
87
88
 
88
89
  const apiKey = apiKeyRaw.trim();
89
90
  const issuerId = issuerIdRaw.trim();
90
- const p8Source = path.resolve(p8PathRaw.trim().replace(/^~/, process.env.HOME || ''));
91
+ const p8Source = path.resolve(p8PathRaw.trim().replace(/^~/, homeDir()));
91
92
 
92
93
  const destPath = await installPrivateKey(p8Source, apiKey);
93
94
  await writeAppleEnv(projectDir, {
@@ -19,16 +19,8 @@ function generateWebhookKey() {
19
19
  return 'rc_wh_' + crypto.randomBytes(16).toString('hex');
20
20
  }
21
21
 
22
- function openUrl(url) {
23
- try {
24
- const { exec } = require('node:child_process');
25
- const cmd = process.platform === 'darwin' ? `open "${url}"`
26
- : process.platform === 'win32' ? `start "" "${url}"`
27
- : `xdg-open "${url}"`;
28
- exec(cmd, { shell: true });
29
- } catch (_) {}
30
- }
31
22
  const ui = require('../utils/ui');
23
+ const { openUrl, promptOpenBrowser } = require('../utils/browser');
32
24
  const { printBanner, successBox, paintLime } = require('../utils/brand');
33
25
  const fs = require('fs-extra');
34
26
  const { createTranslator } = require('../utils/i18n');
@@ -50,7 +42,7 @@ const { generateFirebaseProject } = require('../scaffold/backends/firebase/gener
50
42
  const { generateSupabaseProject } = require('../scaffold/backends/supabase/generator');
51
43
  const { generateApiProject } = require('../scaffold/backends/api/generator');
52
44
  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');
45
+ const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
54
46
  const { toPackageName } = require('../scaffold/backends/firebase/tokens');
55
47
  const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getGcloudInstallInstructions, enableAuthProviders, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
56
48
  const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
@@ -212,7 +204,7 @@ const STEP_LABELS = {
212
204
  'supabase db push': { en: 'Database migrated', pt: 'Banco migrado', es: 'Base de datos migrada' },
213
205
  'anonymous sign-in': { en: 'Anonymous sign-in enabled', pt: 'Login anonimo ativado', es: 'Inicio de sesion anonimo activado' },
214
206
  '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)' },
207
+ '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
208
  'google-auth-options': { en: 'Google client IDs written', pt: 'Client IDs do Google gravados', es: 'Client IDs de Google escritos' },
217
209
  'ios-url-scheme': { en: 'iOS URL scheme registered', pt: 'URL scheme iOS registrado', es: 'URL scheme iOS registrado' },
218
210
  'google-ios-url-scheme': { en: 'iOS Google URL scheme registered', pt: 'URL scheme Google iOS registrado', es: 'URL scheme Google iOS registrado' },
@@ -220,7 +212,7 @@ const STEP_LABELS = {
220
212
  'secret REVENUECAT_WEBHOOK_KEY': { en: 'RevenueCat webhook secret', pt: 'Secret webhook RevenueCat', es: 'Secret webhook RevenueCat' },
221
213
  'secret META_ACCESS_TOKEN': { en: 'Meta Access Token', pt: 'Meta Access Token', es: 'Meta Access Token' },
222
214
  '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' },
215
+ 'fcm-key': { en: 'FCM Service Account key', pt: 'Chave de Service Account FCM', es: 'Clave de Service Account FCM' },
224
216
  'fcm-key-saved': { en: 'FCM key saved to .kasy/', pt: 'Chave FCM salva em .kasy/', es: 'Clave FCM guardada en .kasy/' },
225
217
  'secret FIREBASE_SERVICE_ACCOUNT_JSON': { en: 'FCM Service Account configured', pt: 'Service Account FCM configurado', es: 'Service Account FCM configurado' },
226
218
  'gcp-project': { en: 'GCP project created', pt: 'Projeto GCP criado', es: 'Proyecto GCP creado' },
@@ -233,6 +225,7 @@ const STEP_LABELS = {
233
225
  '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
226
  'service-account': { en: 'Service account key created', pt: 'Chave de conta de servico criada', es: 'Clave de cuenta de servicio creada' },
235
227
  'firestore': { en: 'Firestore database created', pt: 'Banco Firestore criado', es: 'Base de datos Firestore creada' },
228
+ 'firestore-rules': { en: 'Firestore security rules deployed', pt: 'Regras de seguranca Firestore publicadas', es: 'Reglas de seguridad Firestore desplegadas' },
236
229
  'storage': { en: 'Firebase Storage bucket created', pt: 'Bucket Firebase Storage criado', es: 'Bucket Firebase Storage creado' },
237
230
  '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
231
  };
@@ -255,6 +248,7 @@ const STEP_PROGRESS = {
255
248
  '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
249
  'service-account': { en: 'Creating service account key…', pt: 'Criando chave de conta de servico…', es: 'Creando clave de cuenta de servicio…' },
257
250
  'firestore': { en: 'Creating Firestore database…', pt: 'Criando banco Firestore…', es: 'Creando base de datos Firestore…' },
251
+ 'firestore-rules': { en: 'Deploying Firestore security rules…', pt: 'Publicando regras de seguranca Firestore…', es: 'Desplegando reglas de seguridad Firestore…' },
258
252
  'storage': { en: 'Creating Firebase Storage bucket…', pt: 'Criando bucket Firebase Storage…', es: 'Creando bucket Firebase Storage…' },
259
253
  'storage-cors': { en: 'Enabling CORS on Storage…', pt: 'Ativando CORS no Storage…', es: 'Activando CORS en Storage…' },
260
254
  '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)' },
@@ -592,8 +586,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
592
586
  if (billing.ok && billing.accounts?.length > 0) return true;
593
587
  const billingUrl = 'https://console.cloud.google.com/billing/create';
594
588
  ui.log.warn(tr('new.firebase.billing.required'));
595
- ui.note(`${tr('new.firebase.billing.create.steps')}\n${kleur.cyan(billingUrl)}`);
596
- openUrl(billingUrl);
589
+ await promptOpenBrowser({ url: billingUrl, label: tr('new.firebase.billing.create.steps'), t: tr });
597
590
  const ready = await ui.confirm({
598
591
  message: tr('new.firebase.billing.created.ready'),
599
592
  initialValue: true,
@@ -813,11 +806,22 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
813
806
  process.exit(0);
814
807
  }
815
808
  };
816
- const showBeforeContinue = (step1Key, authUrl) => {
817
- ui.note(
818
- `${tr(step1Key)}\n${kleur.cyan(authUrl)}`,
819
- tr('new.firebase.create.beforeContinue.title')
820
- );
809
+ const showBeforeContinue = async (step1Key, authUrl) => {
810
+ // Quick mode keeps prompts to a minimum: show the link and open it
811
+ // automatically. Step-by-step mode uses the Enter-to-open pattern.
812
+ if (isQuick) {
813
+ ui.note(
814
+ `${tr(step1Key)}\n${kleur.cyan(authUrl)}`,
815
+ tr('new.firebase.create.beforeContinue.title')
816
+ );
817
+ openUrl(authUrl);
818
+ return;
819
+ }
820
+ await promptOpenBrowser({
821
+ url: authUrl,
822
+ label: `${tr('new.firebase.create.beforeContinue.title')}\n${tr(step1Key)}`,
823
+ t: tr,
824
+ });
821
825
  };
822
826
  if (setupResult.ok) {
823
827
  ps1.succeed(tr('new.firebase.create.success'));
@@ -829,8 +833,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
829
833
  // retry will activate it once the OAuth Web Client exists. We only warn
830
834
  // here when Email/Anonymous themselves failed (which is the rare path).
831
835
  if (!setupResult.authEnabled) {
832
- showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
833
- openUrl(authUrl);
836
+ await showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
834
837
  if (!isQuick) {
835
838
  await askReady('new.firebase.create.beforeContinue.ready.noAuth');
836
839
  }
@@ -886,8 +889,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
886
889
  printCreateFromScratchStatus(lastResult, tr);
887
890
  const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
888
891
  if (!lastResult.authEnabled) {
889
- showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
890
- openUrl(authUrl);
892
+ await showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
891
893
  if (!isQuick) {
892
894
  await askReady('new.firebase.create.beforeContinue.ready.noAuth');
893
895
  }
@@ -1215,16 +1217,23 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1215
1217
  // --with flag was passed: use those modules directly, skip preset prompt.
1216
1218
  modules = preselectedModules;
1217
1219
  } else if (isQuick) {
1218
- // Quick mode: ship all features by default. Facebook is excluded because
1219
- // it requires App ID + token that we can't auto-generate — the user adds
1220
- // it later with `kasy add facebook` when they have the credentials.
1221
- 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));
1222
1229
  } else {
1223
1230
  section('new.advanced.section.features');
1224
1231
  // Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
1225
1232
  const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
1226
1233
 
1227
- // 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.
1228
1237
  const isWebExcluded = backend === 'firebase' && firebaseSetupMode === 'create';
1229
1238
 
1230
1239
  // Visual groups in display order (header key → feature ids in this group)
@@ -1479,7 +1488,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1479
1488
  firebaseProjectId: core.firebaseProjectId.trim(),
1480
1489
  modules: modules || [],
1481
1490
  backend,
1482
- 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'),
1483
1494
  supabaseUrl: core.supabaseUrl?.trim(),
1484
1495
  supabaseAnonKey: core.supabaseAnonKey?.trim(),
1485
1496
  apiBaseUrl: core.apiBaseUrl?.trim(),
@@ -1587,38 +1598,66 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1587
1598
  : (supabaseExistingResult?.ok ? { projectRef: supabaseExistingResult.projectRef, dbPassword: supabaseExistingResult.dbPassword } : null);
1588
1599
 
1589
1600
  if (backend === 'supabase') {
1590
- // ── Auto-resolve Google credentials from flutterfire output files ────────
1601
+ // ── Google Sign-In: create a real Google OAuth client, then push it to Supabase ──
1591
1602
  const flutterfireOk = result.steps.find((s) => s.name === 'flutterfire')?.ok;
1592
- 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.
1593
1631
  const fileCreds = await readSupabaseGoogleCredentials(targetDir);
1594
1632
  googleWebClientId = fileCreds.webClientId;
1595
1633
  googleIosClientId = fileCreds.iosClientId;
1596
1634
 
1597
- // Enable Google Sign-In in Firebase Auth so Identity Toolkit stores the client secret.
1598
- // This is the same step that setupFromScratch runs for Firebase backend.
1599
- // For Supabase, Firebase Auth is not used for auth, but enabling Google here is the
1600
- // only way to make the client secret available via the Identity Toolkit API.
1601
- // Non-fatal: silently ignored if it fails (e.g. API not yet propagated).
1602
- if (answers.firebaseProjectId) {
1603
- const authResult = await enableAuthProviders(answers.firebaseProjectId);
1604
- if (authResult.ok && !authResult.googleSignInSkipped) {
1605
- printStepResult({ name: 'google sign-in', ok: true }, language);
1606
- }
1607
- if (authResult.appleEnabled) {
1608
- 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
+ }
1609
1643
  }
1610
1644
  }
1611
1645
 
1612
- // Try to get the Client Secret from Identity Toolkit API (requires gcloud auth)
1613
- if (answers.firebaseProjectId && googleWebClientId) {
1614
- googleClientSecret = await getGoogleClientSecretViaGcloud(answers.firebaseProjectId);
1615
- }
1616
-
1617
- // 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.
1618
1647
  if (googleWebClientId) {
1619
1648
  const gaResult = await writeSupabaseGoogleAuthOptions(targetDir, googleWebClientId, googleIosClientId);
1620
1649
  printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
1621
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
+ }
1622
1661
  }
1623
1662
 
1624
1663
  if (supabaseSetupPayload) {
@@ -1631,9 +1670,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1631
1670
  fcmSpinner.stop(tr('new.fcm.generating'));
1632
1671
  if (fcmResult.ok) {
1633
1672
  fcmServiceAccountJson = fcmResult.json;
1634
- printStepResult({ name: 'fcm-key', ok: true }, language);
1673
+ printStepResult({ name: 'fcm-key', ok: true, detail: tr('new.fcm.ok') }, language);
1635
1674
  } else {
1636
- 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);
1637
1676
  }
1638
1677
  }
1639
1678
 
@@ -1657,12 +1696,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1657
1696
  const google = googleWebClientId && googleClientSecret
1658
1697
  ? { webClientId: googleWebClientId, clientSecret: googleClientSecret }
1659
1698
  : {};
1699
+ const apple = answers.bundleId ? { bundleId: answers.bundleId } : {};
1660
1700
  const setupSteps = await setupLinkedProject(
1661
1701
  targetDir,
1662
1702
  supabaseSetupPayload.projectRef,
1663
1703
  supabaseSetupPayload.dbPassword,
1664
1704
  secrets,
1665
- google
1705
+ google,
1706
+ apple
1666
1707
  );
1667
1708
  setupSpinner.stop(tr('new.supabase.setup'));
1668
1709
  setupSteps.forEach((s) => printStepResult(s, language));
@@ -1724,7 +1765,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1724
1765
  printStepResult({ name: 'fcm-key-saved', ok: false, detail: 'salvar arquivo de chave falhou' }, language);
1725
1766
  }
1726
1767
  } else {
1727
- 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);
1728
1769
  }
1729
1770
  }
1730
1771
 
@@ -1750,7 +1791,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1750
1791
  ui.log.warn(
1751
1792
  `${tr('new.sha1.failed', { error: (sha1Result.sha1Error || '').slice(0, 120) })}\n${tr('new.sha1.manual')}\n${kleur.cyan(sha1ManualUrl)}`
1752
1793
  );
1753
- openUrl(sha1ManualUrl);
1754
1794
  }
1755
1795
  }
1756
1796
  }
@@ -1765,12 +1805,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1765
1805
  }
1766
1806
 
1767
1807
  // ── Google + Apple Sign-In: use Firebase CLI for Google (creates OAuth client),
1768
- // then REST API for Apple as best-effort. ───────────────────────────────────
1808
+ // then REST API for Apple as best-effort.
1769
1809
  // Firebase CLI's `deploy --only auth` is the only documented path that auto-
1770
- // creates the OAuth 2.0 Web Client without manual Console clicks the same
1810
+ // creates the OAuth 2.0 Web Client without manual Console clicks, the same
1771
1811
  // backend that the Console hits internally.
1772
- // (Supabase backend keeps the REST-only retry its OAuth client is provisioned
1773
- // differently via the linked Firebase Web app.)
1812
+ // (The Supabase backend runs the same CLI deploy earlier, in its own block, then
1813
+ // pushes the resulting Google + Apple credentials to Supabase via the Mgmt API.)
1774
1814
  if (backend === 'firebase' && answers.firebaseProjectId && flutterfireStep?.ok) {
1775
1815
  const googleSpinner = ui.spinner();
1776
1816
  googleSpinner.start(tr('new.google.enabling'));
@@ -303,6 +303,13 @@ async function runRun(directory, options = {}) {
303
303
  // Firebase Auth persists sessions per-origin (IndexedDB) — a random port
304
304
  // each run means the user gets logged out every restart.
305
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');
306
313
  }
307
314
 
308
315
  // Read dart-defines from .vscode/launch.json (skip if --no-defines)
@@ -1,4 +1,18 @@
1
1
  {
2
+ "1.21.0": {
3
+ "modules": {
4
+ "components": {
5
+ "pt": "KasySidebarPro com renderização mais leve: removidos cálculos de borda redundantes, deixando a sidebar mais simples e performática.",
6
+ "en": "Lighter KasySidebarPro rendering: removed redundant border-radius calculations, making the sidebar simpler and more performant.",
7
+ "es": "Renderizado más ligero de KasySidebarPro: se quitaron cálculos de borde redundantes, dejando la barra lateral más simple y eficiente."
8
+ },
9
+ "core": {
10
+ "pt": "KasyHover agora aceita hoverEnabled: permite desligar o realce ao passar o mouse em listas simples (como a tela de Configurações), mantendo o cursor de clique e o feedback de toque.",
11
+ "en": "KasyHover now accepts hoverEnabled: lets you turn off the hover highlight on plain list rows (like the Settings screen) while keeping the click cursor and press feedback.",
12
+ "es": "KasyHover ahora acepta hoverEnabled: permite desactivar el resaltado al pasar el cursor en listas simples (como la pantalla de Ajustes), manteniendo el cursor de clic y el feedback al pulsar."
13
+ }
14
+ }
15
+ },
2
16
  "1.18.0": {
3
17
  "modules": {
4
18
  "components": {
@@ -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
  ///