kasy-cli 1.32.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 (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `kasy facebook` — guided Facebook Login setup.
3
+ *
4
+ * Facebook needs a Meta app (created manually on developers.facebook.com — no API
5
+ * for that, like Apple). This command guides that manual part (opens the Meta link,
6
+ * tells you exactly what to grab and which redirect URIs to register) and then
7
+ * automates everything that CAN be automated:
8
+ * - writes App ID + Client Token into iOS Info.plist + Android strings.xml
9
+ * - enables the Facebook provider on the backend: Firebase (Identity Toolkit) or
10
+ * Supabase (Management API), using App ID + App Secret
11
+ * - API backend: writes native files only (auth lives on your server)
12
+ *
13
+ * Credentials are cached in ~/.kasy/facebook.json so future projects reuse them
14
+ * (the `kasy new` auto-apply uses the same cache).
15
+ */
16
+
17
+ const path = require('node:path');
18
+ const fs = require('fs-extra');
19
+ const kleur = require('kleur');
20
+ const ui = require('../utils/ui');
21
+ const { printCompactHeader } = require('../utils/brand');
22
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
23
+ const { promptOpenBrowser } = require('../utils/browser');
24
+ const { loadFacebookCreds, saveFacebookCreds, writeFacebookCredentials, setFacebookWebFlag } = require('../utils/facebook');
25
+ const { configureFirebaseFacebook } = require('../scaffold/backends/firebase/setup-from-scratch');
26
+ const { enableFacebookSignIn } = require('../scaffold/backends/supabase/deploy');
27
+
28
+ const META_APPS_URL = 'https://developers.facebook.com/apps';
29
+
30
+ /** Resolve App ID / Client Token / App Secret from flags, then cache, then prompts. */
31
+ async function resolveFbCreds(options, backend, t) {
32
+ const cached = await loadFacebookCreds();
33
+ const needsSecret = backend === 'firebase' || backend === 'supabase';
34
+
35
+ if (options.appId || options.clientToken || options.appSecret) {
36
+ return {
37
+ appId: options.appId || cached?.appId,
38
+ clientToken: options.clientToken || cached?.clientToken,
39
+ appSecret: options.appSecret || cached?.appSecret,
40
+ };
41
+ }
42
+
43
+ if (cached) {
44
+ const reuse = await ui.confirm({
45
+ message: t('facebook.useSaved').replace('{id}', cached.appId),
46
+ initialValue: true,
47
+ });
48
+ if (reuse) return cached;
49
+ }
50
+
51
+ const req = (v) => (v && v.trim() ? undefined : t('facebook.required'));
52
+ const appId = await ui.text({ message: t('facebook.appIdPrompt'), placeholder: '1234567890', validate: req });
53
+ const clientToken = await ui.text({ message: t('facebook.clientTokenPrompt'), validate: req });
54
+ let appSecret = '';
55
+ if (needsSecret) {
56
+ appSecret = await ui.text({ message: t('facebook.appSecretPrompt'), validate: req });
57
+ }
58
+ return { appId: appId.trim(), clientToken: clientToken.trim(), appSecret: (appSecret || '').trim() };
59
+ }
60
+
61
+ /** Push the Facebook provider config to the project's backend. */
62
+ async function applyBackend(backend, projectId, creds, t, s) {
63
+ if (backend === 'firebase') {
64
+ s.start(t('facebook.configuringFirebase'));
65
+ const r = await configureFirebaseFacebook({ projectId, appId: creds.appId, appSecret: creds.appSecret });
66
+ s.stop(r.ok ? t('facebook.firebaseOk') : t('facebook.backendFailed'));
67
+ return r;
68
+ }
69
+ if (backend === 'supabase') {
70
+ s.start(t('facebook.configuringSupabase'));
71
+ const r = await enableFacebookSignIn(projectId, { appId: creds.appId, appSecret: creds.appSecret });
72
+ s.stop(r.ok ? t('facebook.supabaseOk') : t('facebook.backendFailed'));
73
+ return r;
74
+ }
75
+ return { ok: false, skipped: true };
76
+ }
77
+
78
+ async function runFacebook(directory, options = {}) {
79
+ const language = options.language || detectDefaultLanguage();
80
+ const t = createTranslator(language);
81
+ printCompactHeader(t);
82
+ ui.intro(kleur.bold(t('facebook.title')));
83
+
84
+ const projectDir = path.resolve(directory || '.');
85
+ let kit;
86
+ try {
87
+ kit = await fs.readJson(path.join(projectDir, 'kit_setup.json'));
88
+ } catch {
89
+ ui.cancel(t('facebook.notKasyProject'));
90
+ return;
91
+ }
92
+
93
+ const backend = kit.backendProvider || 'firebase';
94
+ const projectId = backend === 'firebase' ? kit.firebaseProjectId : kit.supabaseProjectId;
95
+
96
+ // 1. Guide the manual Meta part (no API to automate it).
97
+ ui.note(t('facebook.metaIntro'), t('facebook.metaTitle'));
98
+ await promptOpenBrowser({
99
+ url: META_APPS_URL,
100
+ label: t('facebook.metaOpenLabel'),
101
+ confirmMessage: t('facebook.metaOpenConfirm'),
102
+ t,
103
+ });
104
+
105
+ // 2. Collect credentials (cache-aware).
106
+ const creds = await resolveFbCreds(options, backend, t);
107
+ if (!creds.appId) {
108
+ ui.cancel(t('facebook.incomplete'));
109
+ return;
110
+ }
111
+ await saveFacebookCreds(creds);
112
+
113
+ // 3. Write the native build-time files (same for every backend).
114
+ const native = await writeFacebookCredentials(projectDir, creds.appId, creds.clientToken);
115
+ if (native.plist === 'ok' || native.strings === 'ok') ui.log.success(t('facebook.nativeOk'));
116
+ else ui.log.warn(t('facebook.nativeMissing'));
117
+
118
+ // 4. Enable the backend provider (Firebase/Supabase). API backend has no managed auth.
119
+ let backendEnabled = false;
120
+ if (backend === 'api') {
121
+ ui.note(t('facebook.backendApi'), t('facebook.metaTitle'));
122
+ } else if (!projectId) {
123
+ ui.log.warn(t('facebook.missingProjectId'));
124
+ } else if (!creds.appSecret) {
125
+ ui.log.warn(t('facebook.noSecret'));
126
+ } else {
127
+ const s = ui.spinner();
128
+ const r = await applyBackend(backend, projectId, creds, t, s);
129
+ backendEnabled = !!r.ok;
130
+ if (!r.ok && !r.skipped) ui.log.error(r.error || 'unknown error');
131
+ }
132
+
133
+ // 5. Web: Facebook on the web works on the Firebase backend (signInWithPopup).
134
+ // Only flip the flag (which shows the web button) AFTER the provider was actually
135
+ // enabled — otherwise the button would be a dead button. On Supabase the web flow
136
+ // isn't wired in the app yet (roadmap), so we never flip it there.
137
+ if (backend === 'firebase' && backendEnabled) {
138
+ const flag = await setFacebookWebFlag(projectDir, true);
139
+ if (flag.ok) ui.log.success(t('facebook.flagUpdated'));
140
+ const redirectUrl = `https://${projectId}.firebaseapp.com/__/auth/handler`;
141
+ ui.note(`${t('facebook.redirectNote')}\n${kleur.cyan(redirectUrl)}`, t('facebook.metaTitle'));
142
+ } else if (backend === 'supabase') {
143
+ ui.log.info(t('facebook.webRoadmap'));
144
+ }
145
+
146
+ ui.outro(t('facebook.allDone'));
147
+ }
148
+
149
+ /**
150
+ * Called by `kasy new`: if Facebook login is enabled in the project AND credentials
151
+ * were saved before (`kasy facebook`), configure it for the fresh project (native
152
+ * files + backend provider) without prompting. Returns status for the success card.
153
+ *
154
+ * @returns {{ applied: boolean, pending: boolean }}
155
+ */
156
+ async function autoApplyFacebookIfCached(targetDir) {
157
+ let kit;
158
+ try {
159
+ kit = await fs.readJson(path.join(targetDir, 'kit_setup.json'));
160
+ } catch {
161
+ return { applied: false, pending: false };
162
+ }
163
+ // The 'facebook' module drives both the login button and the Pixel flag.
164
+ if (!kit.withFacebookPixel) return { applied: false, pending: false };
165
+
166
+ const creds = await loadFacebookCreds();
167
+ if (!creds) return { applied: false, pending: true };
168
+
169
+ // Native files (App ID + Client Token) apply to every backend — Facebook native
170
+ // works on iOS/Android on Firebase and Supabase alike.
171
+ await writeFacebookCredentials(targetDir, creds.appId, creds.clientToken);
172
+
173
+ const backend = kit.backendProvider || 'firebase';
174
+ const projectId = backend === 'firebase' ? kit.firebaseProjectId : kit.supabaseProjectId;
175
+ if ((backend === 'firebase' || backend === 'supabase') && projectId && creds.appSecret) {
176
+ if (backend === 'firebase') {
177
+ await configureFirebaseFacebook({ projectId, appId: creds.appId, appSecret: creds.appSecret });
178
+ } else {
179
+ await enableFacebookSignIn(projectId, { appId: creds.appId, appSecret: creds.appSecret });
180
+ }
181
+ }
182
+ // Show the web Facebook button only where it works in-app (Firebase popup).
183
+ if (backend === 'firebase' && projectId && creds.appSecret) {
184
+ await setFacebookWebFlag(targetDir, true);
185
+ }
186
+ return { applied: true, pending: false };
187
+ }
188
+
189
+ module.exports = { runFacebook, autoApplyFacebookIfCached };
@@ -46,6 +46,8 @@ const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogle
46
46
  const { toPackageName } = require('../scaffold/backends/firebase/tokens');
47
47
  const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, authorizeLocalhostForProject, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
48
48
  const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
49
+ const { autoApplyAppleWebIfCached } = require('./apple-web');
50
+ const { autoApplyFacebookIfCached } = require('./facebook');
49
51
  const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
50
52
 
51
53
  // Default region for creating Supabase projects via the API.
@@ -481,7 +483,7 @@ function printCreateFromScratchStatus(result, tr) {
481
483
  }
482
484
  }
483
485
 
484
- function printSuccessCard(tr, answers, targetDir) {
486
+ function printSuccessCard(tr, answers, targetDir, appleWebStatus = null, facebookStatus = null) {
485
487
  const folderName = path.basename(targetDir);
486
488
 
487
489
  const lines = [];
@@ -509,6 +511,22 @@ function printSuccessCard(tr, answers, targetDir) {
509
511
  }
510
512
  }
511
513
 
514
+ if (appleWebStatus === 'configured') {
515
+ lines.push(paintLime(`✓ ${tr('new.success.appleWeb.configured')}`));
516
+ lines.push('');
517
+ } else if (appleWebStatus === 'pending') {
518
+ lines.push(kleur.yellow(`! ${tr('new.success.appleWeb.pending')}`));
519
+ lines.push('');
520
+ }
521
+
522
+ if (facebookStatus === 'configured') {
523
+ lines.push(paintLime(`✓ ${tr('new.success.facebook.configured')}`));
524
+ lines.push('');
525
+ } else if (facebookStatus === 'pending') {
526
+ lines.push(kleur.yellow(`! ${tr('new.success.facebook.pending')}`));
527
+ lines.push('');
528
+ }
529
+
512
530
  if (answers.backend === 'api') {
513
531
  lines.push(kleur.yellow(`! ${tr('new.success.api.serverContracts')}`));
514
532
  lines.push('');
@@ -1832,22 +1850,24 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1832
1850
  // Supabase setup runs in fcmOnly mode, which intentionally leaves Firebase
1833
1851
  // Auth untouched, so initialize it here (idempotent) before the deploy.
1834
1852
  await ensureFirebaseAuthInitialized(answers.firebaseProjectId);
1835
- // Web Google sign-in on Supabase brokers the Google ID token through the
1836
- // Firebase popup (signInWithPopup), which only runs from an authorized
1837
- // domain. fcmOnly setup skips the authorizedDomains step, so localhost is
1838
- // missing here and the web popup dies with [firebase_auth/unauthorized-domain].
1839
- // Add it best-effort now that auth is initialized. Native (mobile) is unaffected.
1840
- const localhostDomains = await authorizeLocalhostForProject(answers.firebaseProjectId);
1841
- if (!localhostDomains.ok) {
1842
- ui.log.warn(tr('new.google.localhostDomainWarn'));
1843
- }
1844
1853
  const cliResult = await enableAuthViaFirebaseCli({
1845
1854
  projectDir: targetDir,
1846
1855
  projectId: answers.firebaseProjectId,
1847
1856
  appName: answers.appName,
1848
1857
  googleOnly: true,
1849
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);
1850
1867
  googleSpinner.stop(tr('new.google.enabling'));
1868
+ if (!localhostDomains.ok) {
1869
+ ui.log.warn(tr('new.google.localhostDomainWarn'));
1870
+ }
1851
1871
 
1852
1872
  if (cliResult.ok) {
1853
1873
  // The deploy created the OAuth Web Client + iOS client. Re-run flutterfire so
@@ -2118,7 +2138,37 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
2118
2138
  // APNs key (iOS push) is intentionally not mentioned here — it only becomes
2119
2139
  // relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
2120
2140
 
2121
- printSuccessCard(tr, answers, targetDir);
2141
+ // Apple web: auto-configure if the developer saved credentials on a previous
2142
+ // project; otherwise it stays pending and the success card points to the command.
2143
+ let appleWebStatus = null;
2144
+ try {
2145
+ const aw = await autoApplyAppleWebIfCached(targetDir);
2146
+ if (aw.applied) {
2147
+ appleWebStatus = 'configured';
2148
+ ui.log.success(tr('new.appleWeb.autoConfigured'));
2149
+ } else if (aw.pending) {
2150
+ appleWebStatus = 'pending';
2151
+ }
2152
+ } catch (_) {
2153
+ // Non-fatal: web Apple is optional and can be configured later.
2154
+ }
2155
+
2156
+ // Facebook: auto-configure if it's enabled in the project and credentials were
2157
+ // saved on a previous project; otherwise pending and the card points to the command.
2158
+ let facebookStatus = null;
2159
+ try {
2160
+ const fb = await autoApplyFacebookIfCached(targetDir);
2161
+ if (fb.applied) {
2162
+ facebookStatus = 'configured';
2163
+ ui.log.success(tr('new.facebook.autoConfigured'));
2164
+ } else if (fb.pending) {
2165
+ facebookStatus = 'pending';
2166
+ }
2167
+ } catch (_) {
2168
+ // Non-fatal: Facebook is optional and can be configured later.
2169
+ }
2170
+
2171
+ printSuccessCard(tr, answers, targetDir, appleWebStatus, facebookStatus);
2122
2172
 
2123
2173
  ui.outro(kleur.bold(tr('new.success.title')));
2124
2174
  }
@@ -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,31 @@
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
+ },
11
+ "1.34.0": {
12
+ "modules": {
13
+ "core": {
14
+ "pt": "Login com Facebook automatizado + Facebook na web (Firebase): novo comando `kasy facebook` guia os passos na Meta (abre o link), grava App ID + Client Token no iOS/Android e habilita o provedor no Firebase (Identity Toolkit) ou Supabase. No Firebase, o Facebook passa a funcionar na WEB (signInWithPopup) e o botão só aparece quando configurado (flag withFacebookWebSignin). Apple/Facebook na web no Supabase ficam como native-only (roadmap). Corrigido: o `kasy apple-web` agora liga o Apple web só no Firebase (não cria mais botão morto no Supabase). Credenciais ficam salvas e o `kasy new` aplica sozinho.",
15
+ "en": "Facebook Login automated + Facebook on web (Firebase): new `kasy facebook` command guides the Meta steps (opens the link), writes App ID + Client Token into iOS/Android and enables the provider on Firebase (Identity Toolkit) or Supabase. On Firebase, Facebook now works on the WEB (signInWithPopup) and the button only shows once configured (withFacebookWebSignin flag). Apple/Facebook on web for Supabase stay native-only (roadmap). Fixed: `kasy apple-web` now enables Apple web on Firebase only (no more dead button on Supabase). Credentials are cached and `kasy new` applies them automatically.",
16
+ "es": "Inicio de sesión con Facebook automatizado + Facebook en la web (Firebase): nuevo comando `kasy facebook` guía los pasos en Meta (abre el enlace), escribe App ID + Client Token en iOS/Android y habilita el proveedor en Firebase (Identity Toolkit) o Supabase. En Firebase, Facebook ahora funciona en la WEB (signInWithPopup) y el botón solo aparece cuando está configurado (flag withFacebookWebSignin). Apple/Facebook en la web para Supabase quedan como native-only (roadmap). Corregido: `kasy apple-web` ahora activa Apple web solo en Firebase (sin botón muerto en Supabase). Las credenciales se guardan y `kasy new` las aplica automáticamente."
17
+ }
18
+ }
19
+ },
20
+ "1.33.0": {
21
+ "modules": {
22
+ "core": {
23
+ "pt": "Login com Apple na WEB agora é automatizável (backend Firebase): novo comando `kasy apple-web` grava o codeFlowConfig (Service ID + Team ID + Key ID + .p8) no provedor Apple do Firebase, que re-assina o secret sozinho (não expira), reaproveitando suas credenciais salvas. Projetos Firebase novos já nascem com Apple web se você já configurou antes. O botão Apple na web só aparece quando funciona de verdade (sem botão morto); `kasy doctor` mostra se falta configurar. No Supabase, Apple na web é roadmap.",
24
+ "en": "Apple Sign-In on the WEB is now automatable (Firebase backend): new `kasy apple-web` command writes the codeFlowConfig (Service ID + Team ID + Key ID + .p8) into the Firebase Apple provider, which re-signs the secret itself (never expires), reusing your saved credentials. New Firebase projects ship web Apple ready if you configured it before. The web Apple button only shows when it actually works (no dead button); `kasy doctor` reports if it's pending. On Supabase, Apple on web is roadmap.",
25
+ "es": "El inicio de sesión con Apple en la WEB ahora es automatizable (backend Firebase): nuevo comando `kasy apple-web` escribe el codeFlowConfig (Service ID + Team ID + Key ID + .p8) en el proveedor Apple de Firebase, que vuelve a firmar el secret solo (no expira), reutilizando tus credenciales guardadas. Los proyectos Firebase nuevos vienen con Apple web listo si ya lo configuraste antes. El botón de Apple en la web solo aparece cuando funciona de verdad (sin botón muerto); `kasy doctor` indica si falta configurar. En Supabase, Apple en la web es roadmap."
26
+ }
27
+ }
28
+ },
2
29
  "1.32.0": {
3
30
  "modules": {
4
31
  "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: