kasy-cli 1.17.0 → 1.18.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 (90) hide show
  1. package/bin/kasy.js +15 -2
  2. package/lib/commands/add.js +7 -7
  3. package/lib/commands/configure.js +548 -0
  4. package/lib/commands/deploy.js +4 -4
  5. package/lib/commands/doctor.js +17 -0
  6. package/lib/commands/favicon.js +4 -4
  7. package/lib/commands/icon.js +5 -5
  8. package/lib/commands/new.js +403 -238
  9. package/lib/commands/run.js +1 -1
  10. package/lib/commands/splash.js +5 -5
  11. package/lib/commands/update.js +9 -9
  12. package/lib/scaffold/CHANGELOG.json +14 -0
  13. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
  14. package/lib/scaffold/backends/firebase/setup-from-scratch.js +44 -5
  15. package/lib/scaffold/generate.js +24 -8
  16. package/lib/scaffold/shared/post-build.js +8 -0
  17. package/lib/utils/brand.js +16 -12
  18. package/lib/utils/flutter-run.js +139 -11
  19. package/lib/utils/i18n/messages-en.js +58 -5
  20. package/lib/utils/i18n/messages-es.js +58 -5
  21. package/lib/utils/i18n/messages-pt.js +59 -6
  22. package/lib/utils/ui.js +79 -4
  23. package/package.json +1 -1
  24. package/templates/firebase/README.en.md +1 -1
  25. package/templates/firebase/README.es.md +1 -1
  26. package/templates/firebase/README.md +1 -1
  27. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
  28. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
  29. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
  30. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
  31. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
  32. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
  33. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
  34. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
  35. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  41. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  42. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  43. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  44. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  45. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  46. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  47. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  48. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  49. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  50. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
  56. package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
  57. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  58. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  59. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  60. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  61. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
  62. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  63. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  64. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  65. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  66. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  67. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  68. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  69. package/templates/firebase/lib/components/components.dart +1 -0
  70. package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
  71. package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
  72. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  73. package/templates/firebase/lib/components/kasy_date_picker.dart +834 -0
  74. package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
  75. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
  76. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
  77. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
  78. package/templates/firebase/lib/i18n/en.i18n.json +2 -1
  79. package/templates/firebase/lib/i18n/es.i18n.json +2 -1
  80. package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
  81. package/templates/firebase/pubspec.yaml +1 -1
  82. package/templates/firebase/web/index.html +9 -0
  83. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  84. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  85. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  86. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  87. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  88. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  89. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  90. package/templates/firebase/web/splash/img/light-4x.png +0 -0
@@ -29,7 +29,7 @@ function openUrl(url) {
29
29
  } catch (_) {}
30
30
  }
31
31
  const ui = require('../utils/ui');
32
- const { printBanner, infoBox, successBox } = require('../utils/brand');
32
+ const { printBanner, infoBox, successBox, paintLime } = require('../utils/brand');
33
33
  const fs = require('fs-extra');
34
34
  const { createTranslator } = require('../utils/i18n');
35
35
  const { getStoredLanguage, setStoredLanguage } = require('../utils/license');
@@ -42,7 +42,7 @@ const {
42
42
  runChecks,
43
43
  hasRequiredFailures,
44
44
  } = require('../utils/checks');
45
- const { normalizeBackend, getVisibleFeatures } = require('../scaffold/catalog');
45
+ const { normalizeBackend, getVisibleFeatures, FEATURE_CATALOG } = require('../scaffold/catalog');
46
46
 
47
47
  // Audience gate: set KASY_INTERNAL=1 to reveal beta/internal features.
48
48
  const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
@@ -50,9 +50,10 @@ 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 } = require('../scaffold/shared/post-build');
53
+ const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme } = 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
+ const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
56
57
  const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
57
58
 
58
59
  // Região padrão para criação de projetos Supabase via API.
@@ -83,11 +84,9 @@ async function promptBillingAccountIfNeeded(tr, onCancel) {
83
84
  };
84
85
 
85
86
  if (!billingList.accounts?.length) {
86
- ui.note(tr('new.firebase.q.billingAccount.context'));
87
87
  return askManualId();
88
88
  }
89
89
 
90
- ui.note(tr('new.firebase.q.billingAccount.context'));
91
90
  const billingAccountId = await ui.select({
92
91
  message: tr('new.firebase.q.billingAccount'),
93
92
  initialValue: billingList.accounts[0].id,
@@ -213,6 +212,7 @@ const STEP_LABELS = {
213
212
  'pub-get': { en: 'Packages installed', pt: 'Pacotes instalados', es: 'Paquetes instalados' },
214
213
  'slang': { en: 'Translations generated', pt: 'Traducoes geradas', es: 'Traducciones generadas' },
215
214
  'build-runner': { en: 'Code generated (Riverpod / Freezed)', pt: 'Codigo gerado (Riverpod / Freezed)', es: 'Codigo generado (Riverpod / Freezed)' },
215
+ 'dart-fix': { en: 'Lints auto-fixed', pt: 'Avisos de estilo corrigidos', es: 'Lints corregidos automaticamente' },
216
216
  'flutterfire': { en: 'Firebase configured (flutterfire)', pt: 'Firebase configurado (flutterfire)', es: 'Firebase configurado (flutterfire)' },
217
217
  'Service Account key': { en: 'Service Account key', pt: 'Chave de servico', es: 'Clave de servicio' },
218
218
  'firebase_key.json': { en: 'Service account key copied', pt: 'Chave de conta de servico copiada', es: 'Clave de cuenta de servicio copiada' },
@@ -232,6 +232,7 @@ const STEP_LABELS = {
232
232
  'supabase db push': { en: 'Database migrated', pt: 'Banco migrado', es: 'Base de datos migrada' },
233
233
  'anonymous sign-in': { en: 'Anonymous sign-in enabled', pt: 'Login anonimo ativado', es: 'Inicio de sesion anonimo activado' },
234
234
  'google sign-in': { en: 'Google Sign-In enabled', pt: 'Google Sign-In ativado', es: 'Google Sign-In activado' },
235
+ '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)' },
235
236
  'google-auth-options': { en: 'Google client IDs written', pt: 'Client IDs do Google gravados', es: 'Client IDs de Google escritos' },
236
237
  'ios-url-scheme': { en: 'iOS URL scheme registered', pt: 'URL scheme iOS registrado', es: 'URL scheme iOS registrado' },
237
238
  'google-ios-url-scheme': { en: 'iOS Google URL scheme registered', pt: 'URL scheme Google iOS registrado', es: 'URL scheme Google iOS registrado' },
@@ -260,6 +261,7 @@ const STEP_PROGRESS = {
260
261
  'pub-get': { en: 'Installing packages… (may take a few minutes)', pt: 'Instalando pacotes… (pode levar alguns minutos)', es: 'Instalando paquetes… (puede tardar varios minutos)' },
261
262
  'slang': { en: 'Generating translations…', pt: 'Gerando traducoes…', es: 'Generando traducciones…' },
262
263
  'build-runner': { en: 'Generating code (Riverpod / Freezed)… (may take a few minutes)', pt: 'Gerando codigo (Riverpod / Freezed)… (pode demorar)', es: 'Generando codigo (Riverpod / Freezed)… (puede tardar)' },
264
+ 'dart-fix': { en: 'Auto-fixing lints…', pt: 'Corrigindo avisos de estilo…', es: 'Corrigiendo lints automaticamente…' },
263
265
  'flutterfire': { en: 'Connecting to Firebase…', pt: 'Conectando ao Firebase…', es: 'Conectando a Firebase…' },
264
266
  'deploy': { en: 'Deploying backend to Firebase…', pt: 'Publicando backend no Firebase…', es: 'Desplegando backend en Firebase…' },
265
267
  'gcp-project': { en: 'Creating GCP project…', pt: 'Criando projeto GCP…', es: 'Creando proyecto GCP…' },
@@ -324,32 +326,46 @@ function printCreateFromScratchStatus(result, tr) {
324
326
 
325
327
  function printSuccessCard(tr, answers, targetDir) {
326
328
  const folderName = path.basename(targetDir);
327
- const consoleUrl = answers.backend === 'firebase' && answers.firebaseProjectId
328
- ? `https://console.firebase.google.com/project/${answers.firebaseProjectId}`
329
- : answers.backend === 'supabase'
330
- ? 'https://supabase.com/dashboard'
331
- : answers.apiBaseUrl || null;
332
329
 
333
330
  const lines = [];
331
+
332
+ if (answers.modules?.length > 0) {
333
+ const byId = Object.fromEntries(FEATURE_CATALOG.map((f) => [f.id, f]));
334
+ const visible = answers.modules.filter((id) => byId[id]);
335
+ if (visible.length > 0) {
336
+ lines.push(paintLime(`✓ ${tr('new.success.featuresInstalled')}`));
337
+ for (const id of visible) {
338
+ const name = byId[id]?.displayName || id;
339
+ lines.push(` ${kleur.dim('•')} ${name}`);
340
+ }
341
+ lines.push('');
342
+ }
343
+ }
344
+
334
345
  lines.push(kleur.bold(tr('new.success.nextSteps')));
335
346
  lines.push('');
336
- lines.push(`${kleur.dim('1.')} ${kleur.dim(tr('new.success.step.cd'))}`);
347
+
348
+ let stepNum = 1;
349
+ lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.cd'))}`);
337
350
  lines.push(` ${kleur.cyan(`cd ${folderName}`)}`);
338
- let stepNum = 2;
351
+ lines.push('');
352
+
339
353
  if (answers.backend === 'firebase') {
340
- lines.push('');
341
354
  lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.deploy'))}`);
342
355
  lines.push(` ${kleur.cyan('kasy deploy')}`);
356
+ lines.push('');
343
357
  }
358
+
359
+ lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.configure'))}`);
360
+ lines.push(` ${kleur.cyan('kasy configure')}`);
344
361
  lines.push('');
362
+
345
363
  lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.run'))}`);
346
- lines.push(` ${kleur.cyan('kasy run')}`);
347
- lines.push(` ${kleur.dim(tr('new.success.step.run.vscode'))}`);
348
- if (consoleUrl) {
349
- lines.push('');
350
- lines.push(`${kleur.dim(`${stepNum}.`)} ${kleur.dim(tr('new.success.step.console'))}`);
351
- lines.push(` ${kleur.cyan(consoleUrl)}`);
352
- }
364
+ lines.push(` ${kleur.cyan('kasy run')} ${kleur.dim(tr('new.success.step.run.vscode'))}`);
365
+ lines.push('');
366
+
367
+ lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.docs'))}`);
368
+ lines.push(` ${kleur.cyan('https://kasy.dev/docs')}`);
353
369
 
354
370
  console.log(successBox(`🎉 ${tr('new.success.title')}`, lines.join('\n')));
355
371
  }
@@ -464,9 +480,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
464
480
  message: tr('new.q.backend'),
465
481
  initialValue: 'firebase',
466
482
  options: [
467
- { value: 'firebase', label: '🔥 Firebase', hint: tr('new.q.backend.firebase.desc') },
468
- { value: 'supabase', label: '🟢 Supabase', hint: tr('new.q.backend.supabase.desc') },
469
- { value: 'api', label: '🔗 API REST', hint: tr('new.q.backend.api.desc') },
483
+ { value: 'firebase', label: '🔥 Firebase' },
484
+ { value: 'supabase', label: '🟢 Supabase' },
485
+ { value: 'api', label: '🔗 API REST' },
470
486
  ],
471
487
  onCancel: cancel,
472
488
  });
@@ -504,59 +520,38 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
504
520
  }
505
521
  }
506
522
 
507
- // ── App identity: name + bundle ID first things first ────────────────────
508
- let core;
509
- if (yes) {
510
- if (!hasExplicitDir) {
511
- ui.log.error('--yes requires an app name: kasy new MyApp --yes');
512
- ui.cancel(tr('new.firebase.error.aborted'));
513
- process.exit(1);
514
- }
515
- const appName = path.basename(targetDir);
516
- const slug = appName
523
+ // Helper: slug bundle id (e.g. "MeuApp" "com.meuapp.app")
524
+ const deriveBundleId = (name) => {
525
+ const trimmed = (name || '').trim();
526
+ if (!trimmed) return 'com.example.app';
527
+ const slug = trimmed
517
528
  .normalize('NFD')
518
529
  .replace(/[̀-ͯ]/g, '')
519
530
  .toLowerCase()
520
531
  .replace(/[^a-z0-9]/g, '');
521
- const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
522
- core = { appName, bundleId };
523
- ui.log.info(`App: ${kleur.white(appName)}`);
524
- ui.log.info(`Bundle: ${kleur.white(bundleId)}`);
532
+ if (!slug || /^\d/.test(slug)) return 'com.example.app';
533
+ return `com.${slug}.app`;
534
+ };
535
+
536
+ // ── App name — derived from argv when given, otherwise asked once ──────────
537
+ let core;
538
+ if (hasExplicitDir) {
539
+ // `kasy new MeuApp` — use the argument as the name; don't ask again.
540
+ const appName = path.basename(targetDir);
541
+ core = { appName, bundleId: deriveBundleId(appName) };
542
+ ui.log.info(`App: ${kleur.white(core.appName)}`);
543
+ } else if (yes) {
544
+ ui.log.error('--yes requires an app name: kasy new MyApp --yes');
545
+ ui.cancel(tr('new.firebase.error.aborted'));
546
+ process.exit(1);
525
547
  } else {
526
548
  const appName = await ui.text({
527
549
  message: tr('new.firebase.q.appName'),
528
550
  placeholder: tr('new.firebase.q.appName.hint'),
529
- initialValue: hasExplicitDir ? path.basename(targetDir) : '',
530
551
  validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.appName.required')),
531
552
  onCancel: cancel,
532
553
  });
533
-
534
- const defaultBundleId = (() => {
535
- const name = (appName || '').trim();
536
- if (!name) return 'com.example.app';
537
- const slug = name
538
- .normalize('NFD')
539
- .replace(/[̀-ͯ]/g, '')
540
- .toLowerCase()
541
- .replace(/[^a-z0-9]/g, '');
542
- if (!slug || /^\d/.test(slug)) return 'com.example.app';
543
- return `com.${slug}.app`;
544
- })();
545
-
546
- const bundleId = await ui.text({
547
- message: tr('new.firebase.q.bundleId'),
548
- placeholder: tr('new.firebase.q.bundleId.hint'),
549
- initialValue: defaultBundleId,
550
- validate: (v) => {
551
- if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
552
- return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
553
- ? undefined
554
- : tr('new.firebase.q.bundleId.invalid');
555
- },
556
- onCancel: cancel,
557
- });
558
-
559
- core = { appName, bundleId };
554
+ core = { appName, bundleId: deriveBundleId(appName) };
560
555
  }
561
556
 
562
557
  // Resolve targetDir now that we have the app name
@@ -571,10 +566,93 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
571
566
  }
572
567
  }
573
568
 
574
- // ── Firebase setup mode (create vs existing) ────────────────────────────────
575
- // Firebase backend: full setup. Supabase/API: Firebase only for push notifications (FCM).
576
- let firebaseSetupMode = 'existing';
569
+ // ── Wizard mode — Quick (zero config, recommended) or Step-by-step (all options) ─
570
+ let isQuick = yes; // --yes implies Quick mode
577
571
  if (!yes) {
572
+ const wizardMode = await ui.select({
573
+ message: tr('new.q.mode'),
574
+ initialValue: 'quick',
575
+ options: [
576
+ { value: 'quick', label: tr('new.q.mode.quick') },
577
+ { value: 'advanced', label: tr('new.q.mode.advanced') },
578
+ ],
579
+ onCancel: cancel,
580
+ });
581
+ isQuick = wizardMode === 'quick';
582
+ }
583
+
584
+ // ── gcloud is required for Quick mode (creates GCP project + enables Auth via API) ─
585
+ // Failing here, before the project is generated, is much friendlier than dying mid-flow.
586
+ if (isQuick) {
587
+ const gcloudCheck = await checkGcloudAuth();
588
+ if (!gcloudCheck.ok) {
589
+ ui.log.error(tr('new.firebase.create.gcloudRequired'));
590
+ if (gcloudCheck.missing === 'gcloud') {
591
+ const instructions = getGcloudInstallInstructions();
592
+ const noteLines = [tr('new.firebase.create.installTitle')];
593
+ if (instructions.install) noteLines.push(`${tr('new.firebase.create.installCommand')}:\n ${kleur.cyan(instructions.install)}`);
594
+ if (instructions.hint) noteLines.push(instructions.hint);
595
+ noteLines.push(`${tr('new.firebase.create.installAfter')}:\n ${kleur.cyan(instructions.after)}`);
596
+ noteLines.push(`${tr('new.firebase.create.installUrl')}: ${instructions.url}`);
597
+ ui.note(noteLines.join('\n\n'));
598
+ } else {
599
+ ui.note(tr('new.firebase.create.authCommand'));
600
+ }
601
+ ui.cancel(tr('new.firebase.error.aborted'));
602
+ process.exit(1);
603
+ }
604
+
605
+ // ── Billing account check — Firebase needs an active billing account (Blaze) ─
606
+ // Without it, project creation succeeds but Storage and Cloud Functions fail later.
607
+ // Catching it here saves the user from a confusing mid-flow error.
608
+ const ensureBilling = async () => {
609
+ const billing = await listBillingAccounts();
610
+ if (billing.ok && billing.accounts?.length > 0) return true;
611
+ const billingUrl = 'https://console.cloud.google.com/billing/create';
612
+ ui.log.warn(tr('new.firebase.billing.required'));
613
+ ui.note(`${tr('new.firebase.billing.create.steps')}\n${kleur.cyan(billingUrl)}`);
614
+ openUrl(billingUrl);
615
+ const ready = await ui.confirm({
616
+ message: tr('new.firebase.billing.created.ready'),
617
+ initialValue: true,
618
+ onCancel: cancel,
619
+ });
620
+ if (!ready) {
621
+ ui.cancel(tr('new.firebase.error.aborted'));
622
+ process.exit(0);
623
+ }
624
+ const recheck = await listBillingAccounts();
625
+ if (!recheck.ok || !recheck.accounts?.length) {
626
+ ui.log.error(tr('new.firebase.billing.stillMissing'));
627
+ ui.cancel(tr('new.firebase.error.aborted'));
628
+ process.exit(1);
629
+ }
630
+ return true;
631
+ };
632
+ await ensureBilling();
633
+ }
634
+
635
+ // ── Bundle ID — Quick uses derived value silently; Step-by-step lets user override ─
636
+ if (!isQuick) {
637
+ const bundleId = await ui.text({
638
+ message: tr('new.firebase.q.bundleId'),
639
+ placeholder: tr('new.firebase.q.bundleId.hint'),
640
+ initialValue: core.bundleId,
641
+ validate: (v) => {
642
+ if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
643
+ return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
644
+ ? undefined
645
+ : tr('new.firebase.q.bundleId.invalid');
646
+ },
647
+ onCancel: cancel,
648
+ });
649
+ core.bundleId = bundleId;
650
+ }
651
+
652
+ // ── Firebase setup mode — Quick always creates a new project ───────────────
653
+ // (--yes implies Quick, so it also defaults to creating a fresh project.)
654
+ let firebaseSetupMode = 'create';
655
+ if (!isQuick) {
578
656
  if (backend === 'firebase') {
579
657
  firebaseSetupMode = await ui.select({
580
658
  message: tr('new.firebase.q.setupMode'),
@@ -599,25 +677,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
599
677
  }
600
678
  }
601
679
 
602
- // ── Wizard modeQuick (few questions) or Full (all options) ───────────────
603
- // Asked after app name + setup context are clear, so the user understands what it controls.
604
- let isQuick = yes; // --yes implies Quick mode
605
- if (!yes) {
606
- const wizardMode = await ui.select({
607
- message: tr('new.q.mode'),
608
- initialValue: 'quick',
609
- options: [
610
- { value: 'quick', label: tr('new.q.mode.quick') },
611
- { value: 'advanced', label: tr('new.q.mode.advanced') },
612
- ],
613
- onCancel: cancel,
614
- });
615
- isQuick = wizardMode === 'quick';
680
+ // ── Backend-specific prerequisitesonly shown in Step-by-step mode ──────
681
+ // Quick mode hides this list: it intimidates beginners and the CLI handles
682
+ // most prerequisites automatically (gcloud check below, billing prompt, etc.).
683
+ if (!isQuick) {
684
+ printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
616
685
  }
617
686
 
618
- // ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
619
- printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
620
-
621
687
  // ── Firebase project ID (if using an existing project) ──────────────────────
622
688
  if (!yes) {
623
689
  const needFirebaseProjectId =
@@ -639,19 +705,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
639
705
  core.firebaseProjectId = firebaseProjectId;
640
706
  }
641
707
  } else {
642
- let firebaseProjectId = projectHint?.trim() || '';
643
- if (!firebaseProjectId && backend === 'firebase') {
644
- const pid = await ui.text({
645
- message: tr('new.firebase.q.projectId'),
646
- placeholder: tr('new.firebase.q.projectId.hint'),
647
- validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
648
- onCancel: cancel,
649
- });
650
- firebaseProjectId = pid?.trim() || '';
651
- }
652
- if (firebaseProjectId) {
653
- core.firebaseProjectId = firebaseProjectId;
654
- ui.log.info(`Project: ${kleur.white(firebaseProjectId)}`);
708
+ // --yes mode: if --project was passed, reuse that existing project.
709
+ // Otherwise stay in 'create' mode and let setupFromScratch handle creation silently.
710
+ if (projectHint?.trim()) {
711
+ firebaseSetupMode = 'existing';
712
+ core.firebaseProjectId = projectHint.trim();
713
+ ui.log.info(`Project: ${kleur.white(core.firebaseProjectId)}`);
655
714
  }
656
715
  }
657
716
 
@@ -674,11 +733,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
674
733
  // ── Firebase: create from scratch (when selected) ─────────────────────────
675
734
  let firebaseIncludeWeb = true;
676
735
  if (backend === 'firebase' && firebaseSetupMode === 'create') {
677
- firebaseIncludeWeb = await ui.confirm({
678
- message: tr('new.firebase.create.includeWeb'),
679
- initialValue: true,
680
- onCancel: cancel,
681
- });
736
+ if (!isQuick) {
737
+ firebaseIncludeWeb = await ui.confirm({
738
+ message: tr('new.firebase.create.includeWeb'),
739
+ initialValue: true,
740
+ onCancel: cancel,
741
+ });
742
+ }
682
743
  const gcloudCheck = await checkGcloudAuth();
683
744
  if (!gcloudCheck.ok) {
684
745
  ui.log.error(tr('new.firebase.create.gcloudRequired'));
@@ -701,30 +762,50 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
701
762
  onCancel: cancel,
702
763
  });
703
764
  } else {
765
+ // Warn about duration + network before the org/billing prompts so the user
766
+ // can step away (or check the wifi) before the long-running call starts.
767
+ ui.log.info(tr('new.firebase.create.estimatedTime'));
704
768
  const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
705
769
  let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
706
- ui.log.info(`${tr('new.firebase.create.estimatedTime')}\n${tr('new.internet.warning')}`);
707
- const ps1 = ui.makeTimedStepper();
770
+ const ps1 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
708
771
  ps1.next(tr('new.firebase.create.creating'));
709
- const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
772
+ const ps1OnProgress = (key, data) => {
773
+ if (key === 'wait-propagate') {
774
+ ps1.next(tr('new.firebase.create.waitPropagate'));
775
+ } else if (key === 'firestore') {
776
+ ps1.next(stepProgress('firestore', language));
777
+ } else if (key === 'storage') {
778
+ ps1.next(stepProgress('storage', language));
779
+ } else if (key === 'auth-providers-warn') {
780
+ ps1.stop();
781
+ ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
782
+ }
783
+ };
784
+ let setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
710
785
  includeWeb: firebaseIncludeWeb,
711
786
  region: firebaseRegion,
712
787
  tr,
713
788
  billingAccountId: selectedBillingId || undefined,
714
789
  organizationId: selectedOrgId || undefined,
715
- onProgress: (key, data) => {
716
- if (key === 'wait-propagate') {
717
- ps1.next(tr('new.firebase.create.waitPropagate'));
718
- } else if (key === 'firestore') {
719
- ps1.next(stepProgress('firestore', language));
720
- } else if (key === 'storage') {
721
- ps1.next(stepProgress('storage', language));
722
- } else if (key === 'auth-providers-warn') {
723
- ps1.stop();
724
- ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
725
- }
726
- },
790
+ onProgress: ps1OnProgress,
727
791
  });
792
+
793
+ // Silent retry for transient billing-quota errors (propagation delay,
794
+ // race between createProject and linkBilling). Keeps the same spinner
795
+ // line so the user doesn't see the red error if it succeeds on retry.
796
+ if (!setupResult.ok && setupResult.billingFailed && setupResult.billingQuotaError && setupResult.projectId) {
797
+ ps1.update(tr('new.firebase.create.billingWait'));
798
+ await new Promise((r) => setTimeout(r, 15000));
799
+ setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
800
+ includeWeb: firebaseIncludeWeb,
801
+ region: firebaseRegion,
802
+ tr,
803
+ resumeFromBilling: { projectId: setupResult.projectId },
804
+ billingAccountId: selectedBillingId || undefined,
805
+ organizationId: selectedOrgId || undefined,
806
+ onProgress: ps1OnProgress,
807
+ });
808
+ }
728
809
  const askReady = async (readyKey) => {
729
810
  const ok = await ui.confirm({
730
811
  message: tr(readyKey),
@@ -748,26 +829,25 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
748
829
  ui.log.info(`Project ID: ${core.firebaseProjectId}`);
749
830
  printCreateFromScratchStatus(setupResult, tr);
750
831
  const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
751
- if (setupResult.authEnabled && !setupResult.googleSignInSkipped) {
752
- // Email/Password, Anonymous AND Google Sign-In were all enabled automatically skip prompt.
753
- } else {
754
- // At least one provider needs manual activation.
755
- const step1Key = setupResult.googleSignInSkipped
756
- ? 'new.firebase.create.beforeContinue.step1'
757
- : 'new.firebase.create.beforeContinue.step1.noAuth';
758
- const readyKey = setupResult.googleSignInSkipped
759
- ? 'new.firebase.create.beforeContinue.ready'
760
- : 'new.firebase.create.beforeContinue.ready.noAuth';
761
- showBeforeContinue(step1Key, authUrl);
832
+ // Stay silent when Google is the only thing missing — the post-flutterfire
833
+ // retry will activate it once the OAuth Web Client exists. We only warn
834
+ // here when Email/Anonymous themselves failed (which is the rare path).
835
+ if (!setupResult.authEnabled) {
836
+ showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
762
837
  openUrl(authUrl);
763
- await askReady(readyKey);
838
+ if (!isQuick) {
839
+ await askReady('new.firebase.create.beforeContinue.ready.noAuth');
840
+ }
764
841
  }
765
842
 
766
843
  } else {
767
844
  ps1.fail(tr('new.firebase.create.failed'));
768
845
  let lastResult = setupResult;
769
846
  while (lastResult.billingFailed && lastResult.projectId) {
770
- ui.log.error(`${tr('new.firebase.create.failed')}: ${lastResult.error}`);
847
+ const errLine = lastResult.billingQuotaError
848
+ ? tr('new.firebase.create.billingQuotaError')
849
+ : `${tr('new.firebase.create.failed')}: ${lastResult.error}`;
850
+ ui.log.error(errLine);
771
851
  ui.note(
772
852
  `${kleur.cyan(lastResult.billingManualLink)}\n\n${tr('new.firebase.create.billingRetry.hint')}`,
773
853
  tr('new.firebase.create.billingRetry.title')
@@ -784,7 +864,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
784
864
  }
785
865
  ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
786
866
  selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
787
- const ps2 = ui.makeTimedStepper();
867
+ const ps2 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
788
868
  ps2.next(tr('new.firebase.create.creating'));
789
869
  lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
790
870
  includeWeb: firebaseIncludeWeb,
@@ -809,19 +889,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
809
889
  ui.log.info(`Project ID: ${core.firebaseProjectId}`);
810
890
  printCreateFromScratchStatus(lastResult, tr);
811
891
  const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
812
- if (lastResult.authEnabled && !lastResult.googleSignInSkipped) {
813
- // All three providers enabled automatically — skip prompt.
814
- } else {
815
- const step1Key = lastResult.googleSignInSkipped
816
- ? 'new.firebase.create.beforeContinue.step1'
817
- : 'new.firebase.create.beforeContinue.step1.noAuth';
818
- const lastReadyKey = lastResult.googleSignInSkipped
819
- ? 'new.firebase.create.beforeContinue.ready'
820
- : 'new.firebase.create.beforeContinue.ready.noAuth';
821
- showBeforeContinue(step1Key, authUrl);
892
+ if (!lastResult.authEnabled) {
893
+ showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
822
894
  openUrl(authUrl);
823
- await askReady(lastReadyKey);
895
+ if (!isQuick) {
896
+ await askReady('new.firebase.create.beforeContinue.ready.noAuth');
897
+ }
824
898
  }
899
+ // Google Sign-In status is reported after flutterfire (retry path).
825
900
 
826
901
  break;
827
902
  } else {
@@ -874,10 +949,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
874
949
  onCancel: cancel,
875
950
  });
876
951
  } else {
952
+ // Warn before the org/billing prompts so the user is ready for the long call.
953
+ ui.log.info(tr('new.firebase.create.estimatedTime'));
877
954
  const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
878
955
  const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
879
- ui.log.info(tr('new.internet.warning'));
880
- const ps3 = ui.makeTimedStepper();
956
+ const ps3 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
881
957
  ps3.next(tr('new.firebase.create.creatingPush'));
882
958
  const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
883
959
  includeWeb: true,
@@ -926,15 +1002,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
926
1002
  let supabaseExistingResult = null;
927
1003
 
928
1004
  if (backend === 'supabase') {
929
- supabaseCreate = await ui.select({
930
- message: tr('new.supabase.q.create'),
931
- initialValue: true,
932
- options: [
933
- { value: true, label: tr('new.supabase.q.create.create') },
934
- { value: false, label: tr('new.supabase.q.create.existing') },
935
- ],
936
- onCancel: cancel,
937
- });
1005
+ if (isQuick) {
1006
+ supabaseCreate = true;
1007
+ } else {
1008
+ supabaseCreate = await ui.select({
1009
+ message: tr('new.supabase.q.create'),
1010
+ initialValue: true,
1011
+ options: [
1012
+ { value: true, label: tr('new.supabase.q.create.create') },
1013
+ { value: false, label: tr('new.supabase.q.create.existing') },
1014
+ ],
1015
+ onCancel: cancel,
1016
+ });
1017
+ }
938
1018
 
939
1019
  const showLoginRequired = () => {
940
1020
  ui.log.warn(`${tr('new.supabase.loginRequired')}\n${kleur.cyan(tr('new.supabase.loginCommand'))}`);
@@ -971,12 +1051,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
971
1051
  });
972
1052
  supabaseRegion = region || DEFAULT_SUPABASE_REGION;
973
1053
  }
974
- const dbPassword = await ui.password({
975
- message: tr('new.supabase.q.dbPassword'),
976
- validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
977
- onCancel: cancel,
978
- });
979
- supabaseDbPassword = dbPassword;
1054
+ if (isQuick) {
1055
+ // Quick mode: generate a strong password and save to .kasy/supabase.json after build.
1056
+ // The user can read it from there or rotate later via Supabase dashboard.
1057
+ supabaseDbPassword = crypto.randomBytes(18).toString('base64')
1058
+ .replace(/[+/=]/g, '')
1059
+ .slice(0, 24);
1060
+ } else {
1061
+ supabaseDbPassword = await ui.password({
1062
+ message: tr('new.supabase.q.dbPassword'),
1063
+ validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
1064
+ onCancel: cancel,
1065
+ });
1066
+ }
980
1067
  ui.log.info(tr('new.internet.warning'));
981
1068
  const createSpinner = ui.timedSpinner();
982
1069
  createSpinner.start(tr('new.supabase.creating'));
@@ -1124,20 +1211,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1124
1211
  // --with flag was passed: use those modules directly, skip preset prompt.
1125
1212
  modules = preselectedModules;
1126
1213
  } else if (isQuick) {
1127
- // Quick mode: show preset picker (no multiselect).
1128
- const preset = await ui.select({
1129
- message: tr('new.q.preset'),
1130
- initialValue: 'starter',
1131
- options: [
1132
- { value: 'starter', label: tr('new.q.preset.starter') },
1133
- { value: 'saas', label: tr('new.q.preset.saas') },
1134
- { value: 'content', label: tr('new.q.preset.content') },
1135
- { value: 'full', label: tr('new.q.preset.full') },
1136
- { value: 'none', label: tr('new.q.preset.none') },
1137
- ],
1138
- onCancel: cancel,
1139
- });
1140
- modules = MODULE_PRESETS[preset] || [];
1214
+ // Quick mode: ship all features by default. Facebook is excluded because
1215
+ // it requires App ID + token that we can't auto-generate — the user adds
1216
+ // it later with `kasy add facebook` when they have the credentials.
1217
+ modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook');
1141
1218
  } else {
1142
1219
  // Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
1143
1220
  const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
@@ -1192,53 +1269,62 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1192
1269
  const moduleAnswers = {};
1193
1270
 
1194
1271
  if (modules.includes('revenuecat')) {
1195
- // Three keys, all optional, but we require at least one. The kasy run
1196
- // command picks the right key based on the device:
1197
- // simulator/emulator RC_TEST_KEY
1198
- // physical iOS → RC_IOS_PROD_KEY (falls back to test if missing)
1199
- // physical Android → RC_ANDROID_PROD_KEY (falls back to test if missing)
1200
- moduleAnswers.rcTestKey = ((await ui.text({
1201
- message: tr('new.firebase.q.revenuecat.test'),
1202
- validate: (v) => {
1203
- const s = (v || '').trim();
1204
- if (!s) return undefined;
1205
- return /^test_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.test.invalid');
1206
- },
1207
- onCancel: cancel,
1208
- })) || '').trim();
1209
- moduleAnswers.rcIosProdKey = ((await ui.text({
1210
- message: tr('new.firebase.q.revenuecat.iosProd'),
1211
- validate: (v) => {
1212
- const s = (v || '').trim();
1213
- if (!s) return undefined;
1214
- return /^appl_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.iosProd.invalid');
1215
- },
1216
- onCancel: cancel,
1217
- })) || '').trim();
1218
- moduleAnswers.rcAndroidProdKey = ((await ui.text({
1219
- message: tr('new.firebase.q.revenuecat.androidProd'),
1220
- validate: (v) => {
1221
- const s = (v || '').trim();
1222
- if (!s) return undefined;
1223
- return /^goog_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.androidProd.invalid');
1224
- },
1225
- onCancel: cancel,
1226
- })) || '').trim();
1227
- if (!moduleAnswers.rcTestKey && !moduleAnswers.rcIosProdKey && !moduleAnswers.rcAndroidProdKey) {
1228
- // Non-blocking: user can fill .env later. Just warn so they're not surprised.
1229
- ui.log.warn(tr('new.firebase.q.revenuecat.atLeastOne'));
1272
+ if (isQuick) {
1273
+ // Quick mode: ship RevenueCat scaffolded with empty keys + default paywall.
1274
+ // The user fills the keys later via `kasy configure revenuecat`.
1275
+ moduleAnswers.rcTestKey = '';
1276
+ moduleAnswers.rcIosProdKey = '';
1277
+ moduleAnswers.rcAndroidProdKey = '';
1278
+ moduleAnswers.defaultPaywall = 'basic';
1279
+ } else {
1280
+ // Three keys, all optional, but we require at least one. The kasy run
1281
+ // command picks the right key based on the device:
1282
+ // simulator/emulator RC_TEST_KEY
1283
+ // physical iOS → RC_IOS_PROD_KEY (falls back to test if missing)
1284
+ // physical Android → RC_ANDROID_PROD_KEY (falls back to test if missing)
1285
+ moduleAnswers.rcTestKey = ((await ui.text({
1286
+ message: tr('new.firebase.q.revenuecat.test'),
1287
+ validate: (v) => {
1288
+ const s = (v || '').trim();
1289
+ if (!s) return undefined;
1290
+ return /^test_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.test.invalid');
1291
+ },
1292
+ onCancel: cancel,
1293
+ })) || '').trim();
1294
+ moduleAnswers.rcIosProdKey = ((await ui.text({
1295
+ message: tr('new.firebase.q.revenuecat.iosProd'),
1296
+ validate: (v) => {
1297
+ const s = (v || '').trim();
1298
+ if (!s) return undefined;
1299
+ return /^appl_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.iosProd.invalid');
1300
+ },
1301
+ onCancel: cancel,
1302
+ })) || '').trim();
1303
+ moduleAnswers.rcAndroidProdKey = ((await ui.text({
1304
+ message: tr('new.firebase.q.revenuecat.androidProd'),
1305
+ validate: (v) => {
1306
+ const s = (v || '').trim();
1307
+ if (!s) return undefined;
1308
+ return /^goog_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.androidProd.invalid');
1309
+ },
1310
+ onCancel: cancel,
1311
+ })) || '').trim();
1312
+ if (!moduleAnswers.rcTestKey && !moduleAnswers.rcIosProdKey && !moduleAnswers.rcAndroidProdKey) {
1313
+ // Non-blocking: user can fill .env later. Just warn so they're not surprised.
1314
+ ui.log.warn(tr('new.firebase.q.revenuecat.atLeastOne'));
1315
+ }
1316
+ moduleAnswers.defaultPaywall = await ui.select({
1317
+ message: tr('new.firebase.q.paywall'),
1318
+ initialValue: 'basic',
1319
+ options: [
1320
+ { value: 'basic', label: 'Basic (list of plans)' },
1321
+ { value: 'withSwitch', label: 'With trial switch' },
1322
+ { value: 'basicRow', label: 'Row + comparison table' },
1323
+ { value: 'minimal', label: 'Minimal (benefits + CTA)' },
1324
+ ],
1325
+ onCancel: cancel,
1326
+ });
1230
1327
  }
1231
- moduleAnswers.defaultPaywall = await ui.select({
1232
- message: tr('new.firebase.q.paywall'),
1233
- initialValue: 'basic',
1234
- options: [
1235
- { value: 'basic', label: 'Basic (list of plans)' },
1236
- { value: 'withSwitch', label: 'With trial switch' },
1237
- { value: 'basicRow', label: 'Row + comparison table' },
1238
- { value: 'minimal', label: 'Minimal (benefits + CTA)' },
1239
- ],
1240
- onCancel: cancel,
1241
- });
1242
1328
  }
1243
1329
 
1244
1330
  // RC web key — only in advanced mode (optional credential, can configure later).
@@ -1314,8 +1400,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1314
1400
  moduleAnswers.llmConfigureLater = true;
1315
1401
  }
1316
1402
 
1317
- // Facebook — required credentials, always ask.
1318
- if (modules.includes('facebook')) {
1403
+ // Facebook — required credentials. Quick mode ships scaffolded but empty
1404
+ // (user adds credentials later via `kasy configure facebook`).
1405
+ if (modules.includes('facebook') && !isQuick) {
1319
1406
  moduleAnswers.fbAppId = await ui.text({
1320
1407
  message: tr('new.firebase.q.facebook.appId'),
1321
1408
  validate: (v) => {
@@ -1329,6 +1416,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1329
1416
  validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.facebook.token.required')),
1330
1417
  onCancel: cancel,
1331
1418
  });
1419
+ } else if (modules.includes('facebook') && isQuick) {
1420
+ moduleAnswers.fbAppId = '';
1421
+ moduleAnswers.fbToken = '';
1332
1422
  }
1333
1423
 
1334
1424
  // Server secrets (webhook, Meta Ads) — skip in quick mode, configure later via `kasy deploy`.
@@ -1384,9 +1474,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1384
1474
  apiBaseUrl: core.apiBaseUrl?.trim(),
1385
1475
  };
1386
1476
 
1387
- printSummary(tr, answers);
1388
-
1389
- if (!yes) {
1477
+ // Step-by-step mode shows summary before generating so the user can confirm.
1478
+ // Quick mode skips it here — the summary is shown at the end with the success card.
1479
+ if (!yes && !isQuick) {
1480
+ printSummary(tr, answers);
1390
1481
  const proceed = await ui.confirm({
1391
1482
  message: tr('new.firebase.confirm.proceed'),
1392
1483
  initialValue: true,
@@ -1399,10 +1490,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1399
1490
  }
1400
1491
 
1401
1492
  // ── Generate ────────────────────────────────────────────────────────────
1402
- // Stepper shows each step closing as and the next starting as ⠙ — so the
1403
- // user gets explicit "X done Y starting" feedback instead of a single
1404
- // spinner with a mutating message.
1405
- const stepper = ui.makeTimedStepper();
1493
+ // Quick: single rolling line that mutates message. Advanced: stack each step.
1494
+ const stepper = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
1406
1495
  // First step started here so even silent prep work shows progress.
1407
1496
  stepper.next(stepProgress('project-setup', language));
1408
1497
 
@@ -1448,6 +1537,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1448
1537
  functionsRegion: firebaseRegion,
1449
1538
  language,
1450
1539
  onProgress,
1540
+ // Google Sign-In is enabled in a later step via Firebase CLI, which is
1541
+ // when the OAuth Web Client + REVERSED_CLIENT_ID get created. Defer the
1542
+ // two patches that depend on those IDs so they don't fail noisily here.
1543
+ deferGoogleAuthPatches: true,
1451
1544
  });
1452
1545
  }
1453
1546
  // Close the last in-flight step. We don't need a label — its own message
@@ -1507,7 +1600,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1507
1600
  // only way to make the client secret available via the Identity Toolkit API.
1508
1601
  // Non-fatal: silently ignored if it fails (e.g. API not yet propagated).
1509
1602
  if (answers.firebaseProjectId) {
1510
- await enableAuthProviders(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);
1609
+ }
1511
1610
  }
1512
1611
 
1513
1612
  // Try to get the Client Secret from Identity Toolkit API (requires gcloud auth)
@@ -1571,6 +1670,31 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1571
1670
  setupSpinner.error(err.message);
1572
1671
  ui.log.warn(tr('new.supabase.setupManual'));
1573
1672
  }
1673
+
1674
+ // Quick mode generates the DB password automatically — persist it so the
1675
+ // user can recover it later (e.g. to log into Supabase Studio).
1676
+ if (isQuick && supabaseCreate && supabaseDbPassword) {
1677
+ try {
1678
+ const kasyDir = path.join(targetDir, '.kasy');
1679
+ await fs.ensureDir(kasyDir);
1680
+ await fs.writeJson(
1681
+ path.join(kasyDir, 'supabase.json'),
1682
+ { projectRef: supabaseSetupPayload.projectRef, dbPassword: supabaseDbPassword },
1683
+ { spaces: 2 }
1684
+ );
1685
+ const gitignorePath = path.join(targetDir, '.gitignore');
1686
+ const gitignoreEntry = '.kasy/supabase.json';
1687
+ if (await fs.pathExists(gitignorePath)) {
1688
+ const existing = await fs.readFile(gitignorePath, 'utf8');
1689
+ if (!existing.includes(gitignoreEntry)) {
1690
+ await fs.appendFile(gitignorePath, `\n# Supabase auto-generated DB password (credentials — do not commit)\n${gitignoreEntry}\n`, 'utf8');
1691
+ }
1692
+ }
1693
+ ui.log.info(tr('new.supabase.passwordSaved'));
1694
+ } catch (_) {
1695
+ // Non-fatal: user can rotate password via Supabase dashboard if needed.
1696
+ }
1697
+ }
1574
1698
  }
1575
1699
  }
1576
1700
 
@@ -1640,16 +1764,57 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1640
1764
  );
1641
1765
  }
1642
1766
 
1643
- // ── APNs reminder (all backends required for iOS push notifications) ───────
1644
- // Cannot be automated: the .p8 key only exists in Apple Developer Portal.
1645
- if (answers.firebaseProjectId) {
1646
- const apnsUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/settings/cloudmessaging`;
1647
- ui.log.warn(
1648
- `${tr('new.apns.warning')}\n${tr('new.apns.step1')}\n${tr('new.apns.step2')}\n${kleur.cyan(apnsUrl)}`
1649
- );
1650
- openUrl(apnsUrl);
1767
+ // ── Google + Apple Sign-In: use Firebase CLI for Google (creates OAuth client),
1768
+ // then REST API for Apple as best-effort. ───────────────────────────────────
1769
+ // 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
1771
+ // 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.)
1774
+ if (backend === 'firebase' && answers.firebaseProjectId && flutterfireStep?.ok) {
1775
+ const googleSpinner = ui.spinner();
1776
+ googleSpinner.start(tr('new.google.enabling'));
1777
+ const cliResult = await enableAuthViaFirebaseCli({
1778
+ projectDir: targetDir,
1779
+ projectId: answers.firebaseProjectId,
1780
+ appName: answers.appName,
1781
+ });
1782
+ googleSpinner.stop(tr('new.google.enabling'));
1783
+ if (cliResult.ok) {
1784
+ printStepResult({ name: 'google sign-in', ok: true }, language);
1785
+
1786
+ // Google deploy created the OAuth Web Client + iOS client. Re-run flutterfire
1787
+ // so google-services.json and GoogleService-Info.plist pick up the new IDs,
1788
+ // then re-apply the two patches that failed during the initial pass.
1789
+ const rerunSpinner = ui.spinner();
1790
+ rerunSpinner.start(tr('new.google.refreshConfigs'));
1791
+ const ffRerun = await flutterfireConfigure(targetDir, answers.firebaseProjectId, {
1792
+ includeWeb: answers.includeWeb !== false,
1793
+ });
1794
+ rerunSpinner.stop(tr('new.google.refreshConfigs'));
1795
+ if (ffRerun.ok) {
1796
+ const gaResult = await writeGoogleAuthOptions(targetDir);
1797
+ printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
1798
+ const iosResult = await writeGoogleIosUrlScheme(targetDir);
1799
+ printStepResult({ name: 'google-ios-url-scheme', ok: iosResult.ok, detail: iosResult.error }, language);
1800
+ }
1801
+ } else {
1802
+ const authUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/authentication/providers`;
1803
+ const reason = cliResult.error === 'support_email_required'
1804
+ ? tr('new.google.manualHint.noEmail')
1805
+ : tr('new.google.manualHint');
1806
+ ui.log.warn(`${reason}\n${kleur.cyan(authUrl)}`);
1807
+ }
1808
+ // Apple Sign-In remains a best-effort REST POST (no CLI support yet).
1809
+ const appleResult = await enableAuthProviders(answers.firebaseProjectId);
1810
+ if (appleResult.appleEnabled) {
1811
+ printStepResult({ name: 'apple sign-in', ok: true }, language);
1812
+ }
1651
1813
  }
1652
1814
 
1815
+ // APNs key (iOS push) is intentionally not mentioned here — it only becomes
1816
+ // relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
1817
+
1653
1818
  printSuccessCard(tr, answers, targetDir);
1654
1819
 
1655
1820
  ui.outro(kleur.bold(tr('new.success.title')));