kasy-cli 1.34.0 → 1.35.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 (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +154 -56
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
package/README.md CHANGED
@@ -51,7 +51,7 @@ The wizard will ask for your app name, bundle ID, and backend. A complete Flutte
51
51
 
52
52
  ## Optional modules
53
53
 
54
- `sentry` · `analytics` · `revenuecat` · `onboarding` · `widget` · `llm_chat` · `facebook` · `local_notifications` · `feedback` · `ci`
54
+ `sentry` · `analytics` · `revenuecat` · `onboarding` · `widget` · `ai_chat` · `facebook` · `local_notifications` · `feedback` · `ci`
55
55
 
56
56
  ## Requirements
57
57
 
package/bin/kasy.js CHANGED
@@ -8,6 +8,7 @@ const { runDoctor } = require('../lib/commands/doctor');
8
8
  const { runFeatures } = require('../lib/commands/features');
9
9
  const { runValidate } = require('../lib/commands/validate');
10
10
  const { runDeployCommand } = require('../lib/commands/deploy');
11
+ const { runReleaseVersion } = require('../lib/commands/release-version');
11
12
  const { runConfigure } = require('../lib/commands/configure');
12
13
  const { runAppleWeb } = require('../lib/commands/apple-web');
13
14
  const { runFacebook } = require('../lib/commands/facebook');
@@ -327,6 +328,27 @@ function buildProgram(language) {
327
328
  t
328
329
  );
329
330
 
331
+ applyLocalizedHelp(
332
+ program
333
+ .command('release-version')
334
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
335
+ .option('-v, --version <version>', 'Version now live in the stores (SemVer, e.g. 1.4.0)')
336
+ .option('-f, --force', 'Make it a forced (blocking) update — sets app_min_version')
337
+ .option('-p, --project <id>', 'Firebase Project ID')
338
+ .option('-y, --yes', 'Skip confirmations (non-interactive / CI)')
339
+ .description(t('cli.command.releaseVersion.description'))
340
+ .action(async (directory, options) => {
341
+ await runReleaseVersion(directory, {
342
+ language,
343
+ version: options.version,
344
+ force: Boolean(options.force),
345
+ project: options.project,
346
+ yes: Boolean(options.yes),
347
+ });
348
+ }),
349
+ t
350
+ );
351
+
330
352
  applyLocalizedHelp(
331
353
  program
332
354
  .command('configure')
@@ -432,7 +454,7 @@ function buildProgram(language) {
432
454
  applyLocalizedHelp(
433
455
  program
434
456
  .command('add')
435
- .argument('[feature]', 'Feature to add (e.g. sentry, analytics, revenuecat, onboarding, llm_chat, ci...)')
457
+ .argument('[feature]', 'Feature to add (e.g. sentry, analytics, revenuecat, onboarding, ai_chat, ci...)')
436
458
  .option('--list', 'List all available features and their current status')
437
459
  .option('--yes', 'Skip interactive prompts (use placeholder values)')
438
460
  .option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
@@ -446,7 +468,7 @@ function buildProgram(language) {
446
468
  applyLocalizedHelp(
447
469
  program
448
470
  .command('remove')
449
- .argument('[feature]', 'Feature to remove (e.g. sentry, analytics, llm_chat, ci...)')
471
+ .argument('[feature]', 'Feature to remove (e.g. sentry, analytics, ai_chat, ci...)')
450
472
  .option('--yes', 'Skip confirmation prompt')
451
473
  .option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
452
474
  .description(t('cli.command.remove.description'))
@@ -17,7 +17,7 @@ kasy new # interativo — pergunta tudo
17
17
  kasy new meu-app # cria na pasta meu-app
18
18
  kasy new --yes # modo rápido, preset Starter
19
19
  kasy new --backend supabase # define backend sem perguntar
20
- kasy new --with llm_chat,sentry # pré-seleciona features
20
+ kasy new --with ai_chat,sentry # pré-seleciona features
21
21
  ```
22
22
 
23
23
  **Opções:**
@@ -36,12 +36,12 @@ kasy new --with llm_chat,sentry # pré-seleciona features
36
36
  Adiciona uma feature a um projeto Kasy existente.
37
37
 
38
38
  ```bash
39
- kasy add llm_chat
39
+ kasy add ai_chat
40
40
  kasy add sentry
41
41
  kasy add revenuecat
42
42
  kasy add --list # lista features disponíveis e status
43
- kasy add --yes llm_chat # sem perguntas interativas
44
- kasy add -d ./outro-app llm_chat
43
+ kasy add --yes ai_chat # sem perguntas interativas
44
+ kasy add -d ./outro-app ai_chat
45
45
  ```
46
46
 
47
47
  **Features disponíveis:**
@@ -54,7 +54,7 @@ kasy add -d ./outro-app llm_chat
54
54
  | `onboarding` | Fluxo de onboarding customizável |
55
55
  | `web` | Suporte a Flutter Web |
56
56
  | `widget` | Home widgets para iOS e Android |
57
- | `llm_chat` | Chat com IA via Cloud/Edge Functions |
57
+ | `ai_chat` | Chat com IA via Cloud/Edge Functions |
58
58
  | `feedback` | Sistema de feedback e feature requests |
59
59
  | `ci` | CI/CD com GitHub Actions / Codemagic |
60
60
 
@@ -203,7 +203,7 @@ lib/
203
203
  home/ # tela principal
204
204
  notifications/ # notificações locais e push
205
205
  settings/ # configurações do app
206
- llm_chat/ # [feature] chat com IA
206
+ ai_chat/ # [feature] chat com IA
207
207
  feedbacks/ # [feature] feedback e feature requests
208
208
  onboarding/ # [feature] onboarding
209
209
  subscription/ # [feature] assinaturas (RevenueCat)
@@ -246,6 +246,6 @@ No desenvolvimento local, ficam em `.env` (copiado de `.env.example`). `--dart-d
246
246
  | `MIXPANEL_TOKEN` | analytics | Token do Mixpanel |
247
247
  | `RC_ANDROID_API_KEY` | revenuecat | Chave Android do RevenueCat |
248
248
  | `RC_IOS_API_KEY` | revenuecat | Chave iOS do RevenueCat |
249
- | `LLM_CHAT_ENDPOINT` | llm_chat | URL da Cloud/Edge Function |
249
+ | `AI_CHAT_ENDPOINT` | ai_chat | URL da Cloud/Edge Function |
250
250
  | `BACKEND_URL` | supabase/api | URL do backend |
251
251
  | `SUPABASE_TOKEN` | supabase | Anon key do Supabase |
@@ -1850,22 +1850,24 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1850
1850
  // Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
1851
1851
  // Auth untouched, so initialize it here (idempotent) before the deploy.
1852
1852
  await ensureFirebaseAuthInitialized(answers.firebaseProjectId);
1853
- // Web Google sign-in on Supabase brokers the Google ID token through the
1854
- // Firebase popup (signInWithPopup), which only runs from an authorized
1855
- // domain. fcmOnly setup skips the authorizedDomains step, so localhost is
1856
- // missing here and the web popup dies with [firebase_auth/unauthorized-domain].
1857
- // Add it best-effort now that auth is initialized. Native (mobile) is unaffected.
1858
- const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
1859
- if (!localhostDomains.ok) {
1860
- ui.log.warn(tr('new.google.localhostDomainWarn'));
1861
- }
1862
1853
  const cliResult = await enableAuthViaFirebaseCli({
1863
1854
  projectDir: targetDir,
1864
1855
  projectId: answers.firebaseProjectId,
1865
1856
  appName: answers.appName,
1866
1857
  googleOnly: true,
1867
1858
  });
1859
+ // Web Google sign-in on Supabase brokers the Google ID token through the
1860
+ // Firebase popup (signInWithPopup), which only runs from an authorized
1861
+ // domain. New projects don't get `localhost` seeded automatically, so the web
1862
+ // popup would die with [firebase_auth/unauthorized-domain]. Authorize it now —
1863
+ // AFTER the deploy, which fully materializes and stabilizes the auth config.
1864
+ // Doing it right after initializeAuth raced the config's propagation and
1865
+ // intermittently failed. Best-effort + retries. Native (mobile) is unaffected.
1866
+ const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
1868
1867
  googleSpinner.stop(tr('new.google.enabling'));
1868
+ if (!localhostDomains.ok) {
1869
+ ui.log.warn(tr('new.google.localhostDomainWarn'));
1870
+ }
1869
1871
 
1870
1872
  if (cliResult.ok) {
1871
1873
  // The deploy created the OAuth Web Client + iOS client. Re-run flutterfire so
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `kasy release-version` — announce a new app version to existing users.
3
+ *
4
+ * The "update available" prompt in the app compares the installed version
5
+ * (read automatically from pubspec.yaml) against two Firebase Remote Config
6
+ * keys:
7
+ *
8
+ * app_latest_version — newer version published to the stores → OPTIONAL update
9
+ * app_min_version — oldest version still allowed → FORCED update
10
+ *
11
+ * Updating those keys is normally a manual step in the Firebase console (on
12
+ * purpose: publishing to the store and *announcing* the update are different
13
+ * moments). This command automates the announcing part. It reuses the existing
14
+ * Firebase CLI login (`firebase login`) — no service account key, nothing billed.
15
+ *
16
+ * Safety: it bumps `app_latest_version` (optional) by default. Forcing an update
17
+ * (`app_min_version`) is opt-in via `--force`, because a wrong value locks every
18
+ * user out of the app with no way to dismiss.
19
+ *
20
+ * It never overwrites the rest of your Remote Config: it fetches the live
21
+ * template, edits only the version keys, and re-publishes.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const path = require('node:path');
27
+ const fs = require('fs-extra');
28
+ const kleur = require('kleur');
29
+ const { exec } = require('node:child_process');
30
+ const { promisify } = require('node:util');
31
+
32
+ const ui = require('../utils/ui');
33
+ const { printCompactHeader } = require('../utils/brand');
34
+ const { createTranslator } = require('../utils/i18n');
35
+ const { getStoredLanguage } = require('../utils/license');
36
+
37
+ const execAsync = promisify(exec);
38
+
39
+ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
40
+ const TEMPLATE_FILE = 'remoteconfig.template.json';
41
+
42
+ // ── Helpers ─────────────────────────────────────────────────────────────────
43
+
44
+ /** Semantic version from `version: 1.2.3+45` (drops the build number). */
45
+ function readPubspecVersion(content) {
46
+ const match = content.match(/^version:\s*(\d+\.\d+\.\d+)\+?\d*\s*$/m);
47
+ return match ? match[1] : null;
48
+ }
49
+
50
+ /** Firebase project id — google-services.json is always present; .firebaserc as fallback. */
51
+ async function detectProjectId(projectDir) {
52
+ const gsPath = path.join(projectDir, 'android', 'app', 'google-services.json');
53
+ if (await fs.pathExists(gsPath)) {
54
+ try {
55
+ const data = await fs.readJson(gsPath);
56
+ if (data?.project_info?.project_id) return data.project_info.project_id;
57
+ } catch (_) {}
58
+ }
59
+ const rcPath = path.join(projectDir, '.firebaserc');
60
+ if (await fs.pathExists(rcPath)) {
61
+ try {
62
+ const rc = await fs.readJson(rcPath);
63
+ if (rc.projects?.default) return rc.projects.default;
64
+ } catch (_) {}
65
+ }
66
+ return null;
67
+ }
68
+
69
+ async function runFirebase(cmd, projectDir) {
70
+ try {
71
+ const { stdout, stderr } = await execAsync(cmd, {
72
+ cwd: projectDir,
73
+ maxBuffer: 50 * 1024 * 1024,
74
+ });
75
+ return { ok: true, stdout, stderr };
76
+ } catch (err) {
77
+ return { ok: false, error: (err.stderr || err.message || '').trim() };
78
+ }
79
+ }
80
+
81
+ /** Set one string parameter, preserving any existing description / metadata. */
82
+ function setStringParam(template, key, value) {
83
+ template.parameters = template.parameters || {};
84
+ const existing = template.parameters[key] || {};
85
+ template.parameters[key] = {
86
+ ...existing,
87
+ defaultValue: { value },
88
+ valueType: 'STRING',
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Ensure firebase.json points the remoteconfig deploy at our template file.
94
+ * Works on every backend: Supabase / API projects may not ship a firebase.json
95
+ * (they use Firebase Remote Config but don't deploy functions), so we create a
96
+ * minimal one rather than failing.
97
+ */
98
+ async function ensureFirebaseJsonWired(projectDir) {
99
+ const fbPath = path.join(projectDir, 'firebase.json');
100
+ const fb = (await fs.pathExists(fbPath)) ? await fs.readJson(fbPath) : {};
101
+ if (fb.remoteconfig && fb.remoteconfig.template === TEMPLATE_FILE) return false;
102
+ fb.remoteconfig = { template: TEMPLATE_FILE };
103
+ await fs.writeJson(fbPath, fb, { spaces: 2 });
104
+ return true;
105
+ }
106
+
107
+ // ── Main ──────────────────────────────────────────────────────────────────
108
+
109
+ async function runReleaseVersion(directory, options = {}) {
110
+ const language = options.language || getStoredLanguage() || 'en';
111
+ const t = createTranslator(language);
112
+ const projectDir = path.resolve(directory || '.');
113
+ const cancel = () => { ui.cancel(t('releaseVersion.aborted')); process.exit(0); };
114
+
115
+ printCompactHeader(t);
116
+ ui.intro(kleur.bold(t('releaseVersion.intro')));
117
+
118
+ // 1. Validate project
119
+ if (!(await fs.pathExists(path.join(projectDir, 'kit_setup.json')))) {
120
+ ui.log.error(t('releaseVersion.notKasy'));
121
+ ui.cancel(t('releaseVersion.aborted'));
122
+ process.exit(1);
123
+ }
124
+
125
+ // 2. Installed version (suggestion)
126
+ let installed = null;
127
+ const pubspecPath = path.join(projectDir, 'pubspec.yaml');
128
+ if (await fs.pathExists(pubspecPath)) {
129
+ installed = readPubspecVersion(await fs.readFile(pubspecPath, 'utf8'));
130
+ }
131
+ if (installed) {
132
+ ui.log.message(kleur.gray(t('releaseVersion.current', { version: installed })));
133
+ }
134
+
135
+ // 3. Firebase project
136
+ const projectId = options.project || (await detectProjectId(projectDir));
137
+ if (!projectId) {
138
+ ui.log.error(t('releaseVersion.noProject'));
139
+ ui.cancel(t('releaseVersion.aborted'));
140
+ process.exit(1);
141
+ }
142
+ ui.log.message(kleur.gray(t('releaseVersion.detectedProject', { id: projectId })));
143
+
144
+ // 4. Which version to announce
145
+ let version = options.version;
146
+ if (version && !SEMVER_RE.test(String(version).trim())) {
147
+ ui.log.error(t('releaseVersion.badVersion'));
148
+ process.exit(1);
149
+ }
150
+ if (!version) {
151
+ version = await ui.text({
152
+ message: t('releaseVersion.qVersion'),
153
+ placeholder: installed || '1.0.0',
154
+ initialValue: installed || '',
155
+ validate: (v) => (SEMVER_RE.test(String(v || '').trim()) ? undefined : t('releaseVersion.badVersion')),
156
+ onCancel: cancel,
157
+ });
158
+ }
159
+ version = String(version).trim();
160
+
161
+ // 5. Optional vs forced (forced is opt-in)
162
+ let forced = Boolean(options.force);
163
+ if (!forced && !options.yes) {
164
+ ui.log.message(kleur.yellow(t('releaseVersion.forcedHint')));
165
+ forced = await ui.confirm({
166
+ message: t('releaseVersion.qForced'),
167
+ initialValue: false,
168
+ onCancel: cancel,
169
+ });
170
+ }
171
+
172
+ // 6. Confirm
173
+ ui.log.message(
174
+ forced
175
+ ? kleur.red(t('releaseVersion.summaryForced', { version }))
176
+ : kleur.cyan(t('releaseVersion.summaryOptional', { version }))
177
+ );
178
+ if (!options.yes) {
179
+ const go = await ui.confirm({ message: t('releaseVersion.qConfirm'), onCancel: cancel });
180
+ if (!go) cancel();
181
+ }
182
+
183
+ // 7. Fetch live template, edit only the version keys, re-publish
184
+ const spinner = ui.spinner();
185
+ spinner.start(t('releaseVersion.fetching'));
186
+
187
+ const templatePath = path.join(projectDir, TEMPLATE_FILE);
188
+ const fetched = await runFirebase(
189
+ `firebase remoteconfig:get --project ${projectId} -o "${templatePath}"`,
190
+ projectDir
191
+ );
192
+
193
+ let template;
194
+ if (fetched.ok && (await fs.pathExists(templatePath))) {
195
+ try {
196
+ template = await fs.readJson(templatePath);
197
+ } catch (_) {
198
+ template = {};
199
+ }
200
+ } else {
201
+ // First-time / empty config — start fresh (don't abort).
202
+ template = {};
203
+ }
204
+ // Drop server-assigned metadata so the deploy creates a clean new version.
205
+ delete template.version;
206
+ delete template.etag;
207
+
208
+ setStringParam(template, 'app_latest_version', version);
209
+ if (forced) setStringParam(template, 'app_min_version', version);
210
+
211
+ await fs.writeJson(templatePath, template, { spaces: 2 });
212
+ await ensureFirebaseJsonWired(projectDir);
213
+
214
+ spinner.message(t('releaseVersion.deploying'));
215
+ const deployed = await runFirebase(
216
+ `firebase deploy --only remoteconfig --project ${projectId}`,
217
+ projectDir
218
+ );
219
+
220
+ if (!deployed.ok) {
221
+ spinner.error(t('releaseVersion.failed'));
222
+ ui.log.error(deployed.error);
223
+ process.exit(1);
224
+ }
225
+
226
+ spinner.stop(t('releaseVersion.done', { version }));
227
+ ui.outro(
228
+ forced
229
+ ? kleur.red(t('releaseVersion.summaryForced', { version }))
230
+ : kleur.cyan(t('releaseVersion.summaryOptional', { version }))
231
+ );
232
+ }
233
+
234
+ module.exports = { runReleaseVersion };
@@ -116,6 +116,29 @@ function applyCoreFiles(projectDir) {
116
116
  return applyFileList(CORE_FILES, projectDir);
117
117
  }
118
118
 
119
+ /**
120
+ * After an update overwrites files, point the developer to the git-based merge.
121
+ * Deliberately short (3 lines) to keep the CLI clean — the full how-to and the AI
122
+ * merge prompt live in the project's AGENTS.md. Best-effort: stays silent if git
123
+ * is unavailable or nothing actually changed (so a clean update prints nothing).
124
+ */
125
+ async function printMergeHint(projectDir, t) {
126
+ let changed = [];
127
+ try {
128
+ const { stdout } = await execAsync('git diff --name-only', { cwd: projectDir, env: augmentedEnv() });
129
+ changed = stdout.split('\n').map((s) => s.trim()).filter(Boolean);
130
+ } catch {
131
+ return; // not a git repo, or git not installed — say nothing
132
+ }
133
+ if (changed.length === 0) return;
134
+ ui.note(
135
+ `${t('update.mergeHint.body', { count: changed.length })}\n` +
136
+ `${kleur.dim(t('update.mergeHint.aiPrompt'))}\n` +
137
+ `${kleur.dim(t('update.mergeHint.seeAgents'))}`,
138
+ t('update.mergeHint.title'),
139
+ );
140
+ }
141
+
119
142
  /** Same detection logic used by add.js and remove.js. */
120
143
  async function getActiveModules(kitSetup, projectDir) {
121
144
  const modules = [];
@@ -214,6 +237,7 @@ async function runUpdate(module, options = {}) {
214
237
 
215
238
  kitSetup.cliVersion = currentVersion;
216
239
  await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
240
+ await printMergeHint(projectDir, t);
217
241
  ui.outro(t('update.componentsSuccess'));
218
242
  return;
219
243
  }
@@ -262,6 +286,7 @@ async function runUpdate(module, options = {}) {
262
286
 
263
287
  kitSetup.cliVersion = currentVersion;
264
288
  await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
289
+ await printMergeHint(projectDir, t);
265
290
  ui.outro(t('update.coreSuccess'));
266
291
  return;
267
292
  }
@@ -301,6 +326,7 @@ async function runUpdate(module, options = {}) {
301
326
  }
302
327
  kitSetup.cliVersion = currentVersion;
303
328
  await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
329
+ await printMergeHint(projectDir, t);
304
330
  ui.outro(t('update.iosRelease.success'));
305
331
  return;
306
332
  }
@@ -392,6 +418,7 @@ async function runUpdate(module, options = {}) {
392
418
  kitSetup.cliVersion = currentVersion;
393
419
  await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
394
420
 
421
+ await printMergeHint(projectDir, t);
395
422
  ui.outro(t('update.success', { module: normalized }));
396
423
  return;
397
424
  }
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.35.0": {
3
+ "modules": {
4
+ "core": {
5
+ "pt": "Confiança pra escalar: (1) trava anti-patch-desatualizado no publish — se um arquivo do kit muda mas o patch Supabase/API não acompanha, o `npm publish` falha apontando o que revisar (nunca mais release com backend regredido em silêncio). (2) Atualizar projeto existente ficou claro: depois de `kasy update core/components/<feature>`, a CLI mostra quais arquivos mudaram e como juntar suas customizações com o novo (inclusive um prompt pronto pra IA); o passo a passo também fica no AGENTS.md do projeto. (3) Login Google na web (Supabase): a CLI agora autoriza o localhost no Firebase depois do deploy e com novas tentativas, então o popup do Google na web para de falhar com \"unauthorized-domain\" em projetos recém-criados. (4) Login com Google na web (Supabase): corrigido o erro \"missing sub claim\" que impedia o login Google no navegador. Na web não existe usuário anônimo, então o app agora cria a conta direto em vez de tentar vincular a um anônimo inexistente (mesmo comportamento defensivo do Firebase). (5) Proporção da web refinada: a escala 0.95 vale só no desktop (tablet, mobile e nativo ficam no tamanho real, igual ao device preview), e a compensação de escala alta do sistema agora olha o tamanho da TELA, não da janela, então só estreitar o navegador não encolhe mais a interface.",
6
+ "en": "Confidence to scale: (1) stale-patch tripwire at publish — if a kit file changes but the Supabase/API patch doesn't follow, `npm publish` fails pointing to what to review (no more silently regressed backends in a release). (2) Updating an existing project is now clear: after `kasy update core/components/<feature>`, the CLI shows which files changed and how to merge your customizations with the new version (including a ready AI prompt); the step-by-step also lives in the project AGENTS.md. (3) Google web login (Supabase): the CLI now authorizes localhost on Firebase after the deploy and with retries, so the web Google popup stops failing with \"unauthorized-domain\" on freshly created projects. (4) Google web login (Supabase): fixed the \"missing sub claim\" error that blocked Google login in the browser. On web there is no anonymous user, so the app now creates the account directly instead of trying to link to a non-existent anonymous one (matching the Firebase backend's defensive behavior). (5) Web proportion refined: the 0.95 scale applies on desktop only (tablet, mobile and native render at real size, matching the device preview), and the high-OS-scale compensation now keys off the SCREEN size, not the window, so merely narrowing the browser no longer shrinks the UI.",
7
+ "es": "Confianza para escalar: (1) tripwire de patch desactualizado en el publish — si un archivo del kit cambia pero el patch Supabase/API no lo sigue, `npm publish` falla indicando qué revisar (nunca más un backend regresado en silencio). (2) Actualizar un proyecto existente quedó claro: tras `kasy update core/components/<feature>`, la CLI muestra qué archivos cambiaron y cómo combinar tus personalizaciones con lo nuevo (incluido un prompt listo para IA); el paso a paso también está en el AGENTS.md del proyecto. (3) Inicio de sesión Google en web (Supabase): la CLI ahora autoriza localhost en Firebase después del deploy y con reintentos, así el popup de Google en web deja de fallar con \"unauthorized-domain\" en proyectos recién creados. (4) Inicio de sesión Google en web (Supabase): corregido el error \"missing sub claim\" que bloqueaba el login con Google en el navegador. En web no hay usuario anónimo, así que la app ahora crea la cuenta directamente en vez de intentar vincular a un anónimo inexistente (igual que el comportamiento defensivo del backend Firebase). (5) Proporción web refinada: la escala 0.95 aplica solo en desktop (tablet, móvil y nativo se ven a tamaño real, igual que el device preview), y la compensación de escala alta del sistema ahora usa el tamaño de la PANTALLA, no de la ventana, así que solo estrechar el navegador ya no encoge la interfaz."
8
+ }
9
+ }
10
+ },
2
11
  "1.34.0": {
3
12
  "modules": {
4
13
  "core": {
@@ -222,6 +222,30 @@ class HttpAuthenticationApi implements AuthenticationApi {
222
222
  @override
223
223
  Future<String?> getCurrentUserDisplayName() async => null;
224
224
 
225
+ @override
226
+ Future<String?> getCurrentUserPhotoUrl() async => null;
227
+
228
+ @override
229
+ Future<List<String>> getLinkedProviders() async => const [];
230
+
231
+ @override
232
+ Future<void> setPassword(String password) {
233
+ // Wire this to your backend, e.g. POST /auth/set-password { password }.
234
+ throw UnimplementedError(
235
+ '❌ Implement setPassword() to send the new password to your backend.',
236
+ );
237
+ }
238
+
239
+ @override
240
+ Future<List<String>> linkableSocialProviders() async => const [];
241
+
242
+ @override
243
+ Future<void> linkSocialProvider(String provider) {
244
+ throw UnimplementedError(
245
+ '❌ Implement linkSocialProvider() to link a social provider on your backend.',
246
+ );
247
+ }
248
+
225
249
  /// Links an anonymous/guest session to a permanent Google account.
226
250
  ///
227
251
  /// Expected flow:
@@ -73,6 +73,21 @@ abstract class AuthenticationApi implements OnStartService {
73
73
 
74
74
  /// Returns the display name of the current user, or null.
75
75
  Future<String?> getCurrentUserDisplayName();
76
+
77
+ /// Returns the photo URL of the current user, or null.
78
+ Future<String?> getCurrentUserPhotoUrl();
79
+
80
+ /// Returns all sign-in providers linked to the current account.
81
+ Future<List<String>> getLinkedProviders();
82
+
83
+ /// Sets or updates an email/password credential for the current user.
84
+ Future<void> setPassword(String password);
85
+
86
+ /// Social providers the current user can still link to their account.
87
+ Future<List<String>> linkableSocialProviders();
88
+
89
+ /// Links a social provider to the current account.
90
+ Future<void> linkSocialProvider(String provider);
76
91
  }
77
92
 
78
93
  class PhoneAlreadyLinkedException implements Exception {
@@ -99,6 +99,18 @@ abstract class AuthenticationRepository {
99
99
 
100
100
  /// Returns the display name of the current user, or null.
101
101
  Future<String?> getCurrentUserDisplayName();
102
+
103
+ /// Returns the photo URL of the current user, or null.
104
+ Future<String?> getCurrentUserPhotoUrl();
105
+
106
+ /// Returns the sign-in provider used ('google'|'apple'|'facebook'|'email'|'phone'), or null.
107
+ Future<List<String>> getLinkedProviders();
108
+
109
+ Future<void> setPassword(String password);
110
+
111
+ Future<List<String>> linkableSocialProviders();
112
+
113
+ Future<void> linkSocialProvider(String provider);
102
114
  }
103
115
 
104
116
  /// this is an example on how to create an authentication repository using firebase
@@ -403,4 +415,19 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
403
415
 
404
416
  @override
405
417
  Future<String?> getCurrentUserDisplayName() => _authenticationApi.getCurrentUserDisplayName();
418
+
419
+ @override
420
+ Future<String?> getCurrentUserPhotoUrl() => _authenticationApi.getCurrentUserPhotoUrl();
421
+
422
+ @override
423
+ Future<List<String>> getLinkedProviders() => _authenticationApi.getLinkedProviders();
424
+
425
+ @override
426
+ Future<void> setPassword(String password) => _authenticationApi.setPassword(password);
427
+
428
+ @override
429
+ Future<List<String>> linkableSocialProviders() => _authenticationApi.linkableSocialProviders();
430
+
431
+ @override
432
+ Future<void> linkSocialProvider(String provider) => _authenticationApi.linkSocialProvider(provider);
406
433
  }
@@ -36,6 +36,7 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
36
36
  @JsonKey(name: 'creation_date') DateTime? creationDate,
37
37
  @JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
38
38
  @JsonKey(name: 'period_end_date') DateTime? periodEndDate,
39
+ @JsonKey(name: 'trial_end') DateTime? trialEnd,
39
40
  @JsonKey(name: 'status') required SubscriptionStatus status,
40
41
  @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
41
42
  SubscriptionStore? store,
@@ -710,37 +710,51 @@ async function checkBillingEnabled(projectId) {
710
710
  * with only [localhost] would wipe firebaseapp.com / web.app. Idempotent: it's a
711
711
  * no-op when both entries are already present.
712
712
  *
713
+ * Right after a project's auth config is initialized, the Admin v2 config endpoint
714
+ * can briefly answer 403/404/409 before it settles (and a PATCH can 409 on a
715
+ * concurrent edit). Those are retried — re-reading each round so the merge stays
716
+ * correct; 400 and other client errors are treated as fatal. Best-effort: returns
717
+ * { ok: false } after exhausting retries instead of throwing.
718
+ *
719
+ * @param {string} projectId
720
+ * @param {string} token
721
+ * @param {{ maxRetries?: number, retryDelayMs?: number }} [opts]
713
722
  * @returns {{ ok: boolean, added?: string[], error?: string }}
714
723
  */
715
- async function ensureLocalhostAuthorizedDomains(projectId, token) {
724
+ async function ensureLocalhostAuthorizedDomains(projectId, token, { maxRetries = 4, retryDelayMs = 4000 } = {}) {
716
725
  const base = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config`;
717
726
  const headers = {
718
727
  Authorization: `Bearer ${token}`,
719
728
  'Content-Type': 'application/json',
720
729
  'X-Goog-User-Project': projectId,
721
730
  };
722
- // 1. Read the current authorized domains.
723
- const getRes = await fetch(base, { headers });
724
- if (!getRes.ok) {
725
- const text = await getRes.text();
726
- return { ok: false, error: `${getRes.status}: ${text}` };
727
- }
728
- const config = await getRes.json();
729
- const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
730
731
  const required = ['localhost', '127.0.0.1'];
731
- const missing = required.filter((d) => !current.includes(d));
732
- if (missing.length === 0) return { ok: true, added: [] };
733
- // 2. Merge and write back, keeping every domain that was already there.
734
- const patchRes = await fetch(`${base}?updateMask=authorizedDomains`, {
735
- method: 'PATCH',
736
- headers,
737
- body: JSON.stringify({ authorizedDomains: [...current, ...missing] }),
738
- });
739
- if (!patchRes.ok) {
740
- const text = await patchRes.text();
741
- return { ok: false, error: `${patchRes.status}: ${text}` };
732
+ const TRANSIENT = new Set([403, 404, 409, 429, 500, 503]);
733
+ let lastError = 'unknown';
734
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
735
+ // 1. Read the current authorized domains.
736
+ const getRes = await fetch(base, { headers });
737
+ if (!getRes.ok) {
738
+ lastError = `GET ${getRes.status}: ${await getRes.text()}`;
739
+ if (attempt < maxRetries && TRANSIENT.has(getRes.status)) { await sleep(retryDelayMs); continue; }
740
+ return { ok: false, error: lastError };
741
+ }
742
+ const config = await getRes.json();
743
+ const current = Array.isArray(config.authorizedDomains) ? config.authorizedDomains : [];
744
+ const missing = required.filter((d) => !current.includes(d));
745
+ if (missing.length === 0) return { ok: true, added: [] };
746
+ // 2. Merge and write back, keeping every domain that was already there.
747
+ const patchRes = await fetch(`${base}?updateMask=authorizedDomains`, {
748
+ method: 'PATCH',
749
+ headers,
750
+ body: JSON.stringify({ authorizedDomains: [...current, ...missing] }),
751
+ });
752
+ if (patchRes.ok) return { ok: true, added: missing };
753
+ lastError = `PATCH ${patchRes.status}: ${await patchRes.text()}`;
754
+ if (attempt < maxRetries && TRANSIENT.has(patchRes.status)) { await sleep(retryDelayMs); continue; }
755
+ return { ok: false, error: lastError };
742
756
  }
743
- return { ok: true, added: missing };
757
+ return { ok: false, error: lastError };
744
758
  }
745
759
 
746
760
  /**