kasy-cli 1.31.14 → 1.34.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 (127) hide show
  1. package/bin/kasy.js +42 -0
  2. package/lib/commands/apple-web.js +222 -0
  3. package/lib/commands/configure.js +3 -91
  4. package/lib/commands/doctor.js +20 -0
  5. package/lib/commands/facebook.js +189 -0
  6. package/lib/commands/new.js +65 -3
  7. package/lib/scaffold/CHANGELOG.json +27 -0
  8. package/lib/scaffold/backends/api/patch/README.md +87 -2
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  11. package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
  12. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  13. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
  14. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
  15. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
  16. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
  18. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  19. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
  20. package/lib/scaffold/generate.js +1 -1
  21. package/lib/scaffold/shared/generator-utils.js +34 -3
  22. package/lib/utils/apple-web.js +147 -0
  23. package/lib/utils/facebook.js +162 -0
  24. package/lib/utils/i18n/messages-en.js +64 -0
  25. package/lib/utils/i18n/messages-es.js +64 -0
  26. package/lib/utils/i18n/messages-pt.js +64 -0
  27. package/package.json +2 -2
  28. package/templates/firebase/AGENTS.md +87 -0
  29. package/templates/firebase/CLAUDE.md +16 -0
  30. package/templates/firebase/DESIGN_SYSTEM.md +234 -0
  31. package/templates/firebase/docs/auth-setup.en.md +7 -1
  32. package/templates/firebase/docs/auth-setup.es.md +7 -1
  33. package/templates/firebase/docs/auth-setup.pt.md +7 -1
  34. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
  35. package/templates/firebase/lib/components/components.dart +1 -0
  36. package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
  37. package/templates/firebase/lib/components/kasy_alert.dart +1 -1
  38. package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
  39. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
  40. package/templates/firebase/lib/components/kasy_button.dart +8 -8
  41. package/templates/firebase/lib/components/kasy_chip.dart +1 -1
  42. package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
  43. package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
  44. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  45. package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
  46. package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
  47. package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
  48. package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
  49. package/templates/firebase/lib/components/kasy_toast.dart +39 -70
  50. package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
  51. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  52. package/templates/firebase/lib/core/config/features.dart +18 -0
  53. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
  54. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
  55. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
  56. package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
  57. package/templates/firebase/lib/core/theme/shadows.dart +13 -0
  58. package/templates/firebase/lib/core/theme/texts.dart +32 -0
  59. package/templates/firebase/lib/core/theme/theme.dart +2 -0
  60. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
  61. package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
  62. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  63. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  66. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
  67. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
  68. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
  69. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
  70. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
  71. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
  72. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
  73. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
  74. package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
  75. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
  76. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
  77. package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
  78. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
  79. package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
  80. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
  81. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  82. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  83. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  84. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
  85. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
  86. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
  87. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
  88. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
  89. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  90. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
  91. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  92. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
  93. package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
  94. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
  95. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
  96. package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
  97. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
  98. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
  99. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
  100. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
  101. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
  102. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
  103. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
  104. package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
  105. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
  106. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
  107. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
  108. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
  109. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
  110. package/templates/firebase/lib/i18n/en.i18n.json +13 -4
  111. package/templates/firebase/lib/i18n/es.i18n.json +13 -4
  112. package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
  113. package/templates/firebase/lib/router.dart +2 -0
  114. package/templates/firebase/pubspec.yaml +1 -2
  115. package/templates/firebase/tool/design_check.dart +152 -0
  116. package/templates/firebase/web/stripe_success.html +64 -26
  117. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  118. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  119. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  120. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
  121. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
  122. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
  123. package/templates/firebase/assets/images/review.png +0 -0
  124. package/templates/firebase/assets/images/update.png +0 -0
  125. package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
  126. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
  127. package/templates/firebase/login-redesign-preview.png +0 -0
package/bin/kasy.js CHANGED
@@ -9,6 +9,8 @@ const { runFeatures } = require('../lib/commands/features');
9
9
  const { runValidate } = require('../lib/commands/validate');
10
10
  const { runDeployCommand } = require('../lib/commands/deploy');
11
11
  const { runConfigure } = require('../lib/commands/configure');
12
+ const { runAppleWeb } = require('../lib/commands/apple-web');
13
+ const { runFacebook } = require('../lib/commands/facebook');
12
14
  const { runCheck } = require('../lib/commands/check');
13
15
  const { runRun } = require('../lib/commands/run');
14
16
  const { runReset } = require('../lib/commands/reset');
@@ -336,6 +338,46 @@ function buildProgram(language) {
336
338
  t
337
339
  );
338
340
 
341
+ applyLocalizedHelp(
342
+ program
343
+ .command('apple-web')
344
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
345
+ .option('--service-id <id>', 'Apple Service ID (the OAuth client_id), e.g. com.acme.app.signin')
346
+ .option('--team-id <id>', 'Apple Developer Team ID')
347
+ .option('--key-id <id>', 'Key ID of the .p8')
348
+ .option('--p8 <path>', 'Path to the Sign In with Apple .p8 private key')
349
+ .description(t('cli.command.appleWeb.description'))
350
+ .action(async (directory, options) => {
351
+ await runAppleWeb(directory, {
352
+ language,
353
+ serviceId: options.serviceId,
354
+ teamId: options.teamId,
355
+ keyId: options.keyId,
356
+ p8: options.p8,
357
+ });
358
+ }),
359
+ t
360
+ );
361
+
362
+ applyLocalizedHelp(
363
+ program
364
+ .command('facebook')
365
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
366
+ .option('--app-id <id>', 'Meta App ID')
367
+ .option('--client-token <token>', 'Meta Client Token (Settings → Advanced)')
368
+ .option('--app-secret <secret>', 'Meta App Secret (for the Firebase/Supabase provider)')
369
+ .description(t('cli.command.facebook.description'))
370
+ .action(async (directory, options) => {
371
+ await runFacebook(directory, {
372
+ language,
373
+ appId: options.appId,
374
+ clientToken: options.clientToken,
375
+ appSecret: options.appSecret,
376
+ });
377
+ }),
378
+ t
379
+ );
380
+
339
381
  applyLocalizedHelp(
340
382
  program
341
383
  .command('check')
@@ -0,0 +1,222 @@
1
+ /**
2
+ * `kasy apple-web` — configure "Sign in with Apple" on the WEB.
3
+ *
4
+ * Apple web in-app is Firebase-only for now. Native iOS/macOS is already enabled by
5
+ * `kasy new` on every backend. The web flow needs a Service ID + a `.p8`, created
6
+ * once on developer.apple.com (no API for that — stays manual). This command:
7
+ * - Firebase: writes the Apple provider's codeFlowConfig (Service ID + Team ID +
8
+ * Key ID + `.p8`) and flips withAppleWebSignin true. Firebase re-signs the secret
9
+ * itself, so it never expires.
10
+ * - Supabase / API: web Apple isn't wired in the app yet (roadmap, see ROADMAP.md).
11
+ * The command only prints a note and configures nothing / flips no flag.
12
+ *
13
+ * The inputs are cached in ~/.kasy/apple-web.json so future Firebase projects
14
+ * configure web Apple without asking again.
15
+ *
16
+ * Note: enableAppleWebSignIn() in supabase/deploy.js is a tested roadmap helper for
17
+ * the future Supabase web flow; it is not invoked by this command yet.
18
+ */
19
+
20
+ const path = require('node:path');
21
+ const os = require('node:os');
22
+ const fs = require('fs-extra');
23
+ const kleur = require('kleur');
24
+ const ui = require('../utils/ui');
25
+ const { printCompactHeader } = require('../utils/brand');
26
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
27
+ const { loadAppleWebCreds, saveAppleWebCreds } = require('../utils/apple-web');
28
+ const { configureFirebaseAppleWeb } = require('../scaffold/backends/firebase/setup-from-scratch');
29
+
30
+ /** Expand a leading ~ to the user's home directory. */
31
+ function expandHome(p) {
32
+ if (!p) return p;
33
+ return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
34
+ }
35
+
36
+ /** Set `withAppleWebSignin` to the given boolean in the project's features.dart. */
37
+ async function setAppleWebFlag(projectDir, value) {
38
+ const file = path.join(projectDir, 'lib', 'core', 'config', 'features.dart');
39
+ if (!(await fs.pathExists(file))) return { ok: false, missing: true };
40
+ let content = await fs.readFile(file, 'utf8');
41
+ const re = /const bool withAppleWebSignin\s*=\s*(?:true|false)\s*;/;
42
+ if (!re.test(content)) return { ok: false, missing: true };
43
+ content = content.replace(re, `const bool withAppleWebSignin = ${value};`);
44
+ await fs.writeFile(file, content, 'utf8');
45
+ return { ok: true };
46
+ }
47
+
48
+ /**
49
+ * Resolve the four Apple inputs: from CLI flags, then the cache, then prompts.
50
+ * Reads the `.p8` from a file path when given. Returns null on any read failure.
51
+ */
52
+ async function resolveCreds(options, t) {
53
+ const cached = await loadAppleWebCreds();
54
+
55
+ // Non-interactive / flag-driven path.
56
+ if (options.serviceId || options.teamId || options.keyId || options.p8) {
57
+ const privateKey = options.p8
58
+ ? await fs.readFile(expandHome(options.p8), 'utf8').catch(() => null)
59
+ : cached?.privateKey;
60
+ if (options.p8 && !privateKey) {
61
+ ui.log.error(t('appleWeb.p8ReadError'));
62
+ return null;
63
+ }
64
+ return {
65
+ serviceId: options.serviceId || cached?.serviceId,
66
+ teamId: options.teamId || cached?.teamId,
67
+ keyId: options.keyId || cached?.keyId,
68
+ privateKey,
69
+ };
70
+ }
71
+
72
+ // Reuse the cache without asking, unless the user wants to replace it.
73
+ if (cached) {
74
+ const reuse = await ui.confirm({
75
+ message: t('appleWeb.useSaved').replace('{id}', cached.serviceId),
76
+ initialValue: true,
77
+ });
78
+ if (reuse) return cached;
79
+ }
80
+
81
+ // Interactive prompts.
82
+ const serviceId = await ui.text({
83
+ message: t('appleWeb.serviceIdPrompt'),
84
+ placeholder: 'com.acme.app.signin',
85
+ validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
86
+ });
87
+ const teamId = await ui.text({
88
+ message: t('appleWeb.teamIdPrompt'),
89
+ placeholder: 'ABCDE12345',
90
+ validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
91
+ });
92
+ const keyId = await ui.text({
93
+ message: t('appleWeb.keyIdPrompt'),
94
+ placeholder: '6RR89XG535',
95
+ validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
96
+ });
97
+ const p8Path = await ui.text({
98
+ message: t('appleWeb.p8Prompt'),
99
+ placeholder: '~/Downloads/AuthKey_6RR89XG535.p8',
100
+ validate: (v) => (v && v.trim() ? undefined : t('appleWeb.required')),
101
+ });
102
+ const privateKey = await fs.readFile(expandHome(p8Path.trim()), 'utf8').catch(() => null);
103
+ if (!privateKey) {
104
+ ui.log.error(t('appleWeb.p8ReadError'));
105
+ return null;
106
+ }
107
+ return {
108
+ serviceId: serviceId.trim(),
109
+ teamId: teamId.trim(),
110
+ keyId: keyId.trim(),
111
+ privateKey,
112
+ };
113
+ }
114
+
115
+ async function runAppleWeb(directory, options = {}) {
116
+ const language = options.language || detectDefaultLanguage();
117
+ const t = createTranslator(language);
118
+ printCompactHeader(t);
119
+ ui.intro(kleur.bold(t('appleWeb.title')));
120
+
121
+ const projectDir = path.resolve(directory || '.');
122
+ const kitSetupPath = path.join(projectDir, 'kit_setup.json');
123
+ if (!(await fs.pathExists(kitSetupPath))) {
124
+ ui.cancel(t('appleWeb.notKasyProject'));
125
+ return;
126
+ }
127
+
128
+ let kit;
129
+ try {
130
+ kit = await fs.readJson(kitSetupPath);
131
+ } catch {
132
+ ui.cancel(t('appleWeb.notKasyProject'));
133
+ return;
134
+ }
135
+
136
+ const backend = kit.backendProvider || 'firebase';
137
+ const bundleId = kit.bundleId || null;
138
+
139
+ // Apple web in-app works on the Firebase backend only (signInWithPopup). On
140
+ // Supabase the web flow isn't wired in the app yet (roadmap) and the API backend
141
+ // owns its own auth — in both cases there's nothing to configure here for web Apple
142
+ // (native iOS is already set up by `kasy new`).
143
+ if (backend !== 'firebase') {
144
+ ui.note(t('appleWeb.notFirebase'), t('appleWeb.manualTitle'));
145
+ ui.outro(t('appleWeb.notFirebaseDone'));
146
+ return;
147
+ }
148
+
149
+ const projectId = kit.firebaseProjectId;
150
+ if (!projectId) {
151
+ ui.cancel(t('appleWeb.missingProjectId'));
152
+ return;
153
+ }
154
+
155
+ const creds = await resolveCreds(options, t);
156
+ if (!creds) return;
157
+ if (!creds.serviceId || !creds.teamId || !creds.keyId || !creds.privateKey) {
158
+ ui.cancel(t('appleWeb.incomplete'));
159
+ return;
160
+ }
161
+
162
+ // Cache for future projects.
163
+ await saveAppleWebCreds(creds);
164
+
165
+ const s = ui.spinner();
166
+ s.start(t('appleWeb.configuringFirebase'));
167
+ const result = await configureFirebaseAppleWeb({ projectId, bundleId, ...creds });
168
+ if (!result.ok) {
169
+ s.stop(t('appleWeb.failed'));
170
+ ui.log.error(result.error || 'unknown error');
171
+ return;
172
+ }
173
+ s.stop(t('appleWeb.firebaseOk'));
174
+
175
+ // Show the Apple button on web now that it actually works.
176
+ const flag = await setAppleWebFlag(projectDir, true);
177
+ if (flag.ok) ui.log.success(t('appleWeb.flagUpdated'));
178
+ else ui.log.warn(t('appleWeb.flagMissing'));
179
+
180
+ // Remind the developer of the manual Return URL their Service ID must carry.
181
+ const returnUrl = `https://${projectId}.firebaseapp.com/__/auth/handler`;
182
+ ui.note(`${t('appleWeb.returnUrlNote')}\n${kleur.cyan(returnUrl)}`, t('appleWeb.manualTitle'));
183
+ ui.outro(t('appleWeb.allDone'));
184
+ }
185
+
186
+ /**
187
+ * Called by `kasy new`: if the developer already saved Apple web credentials on a
188
+ * previous project (`kasy apple-web`), configure web Apple for the freshly created
189
+ * project automatically — same behavior in both quick and advanced modes. No
190
+ * prompts: it either applies (creds cached) or reports pending.
191
+ *
192
+ * @param {string} targetDir - the generated project directory
193
+ * @returns {{ applied: boolean, pending: boolean, backend?: string, error?: string }}
194
+ */
195
+ async function autoApplyAppleWebIfCached(targetDir) {
196
+ let kit;
197
+ try {
198
+ kit = await fs.readJson(path.join(targetDir, 'kit_setup.json'));
199
+ } catch {
200
+ return { applied: false, pending: false };
201
+ }
202
+
203
+ const backend = kit.backendProvider || 'firebase';
204
+ // Apple web in-app is Firebase-only for now (Supabase web = roadmap, API = own
205
+ // server). On those backends there's nothing to apply and nothing pending.
206
+ if (backend !== 'firebase') return { applied: false, pending: false, backend };
207
+
208
+ const projectId = kit.firebaseProjectId;
209
+ if (!projectId) return { applied: false, pending: false, backend };
210
+
211
+ const creds = await loadAppleWebCreds();
212
+ if (!creds) return { applied: false, pending: true, backend };
213
+
214
+ const bundleId = kit.bundleId || null;
215
+ const result = await configureFirebaseAppleWeb({ projectId, bundleId, ...creds });
216
+ if (!result.ok) return { applied: false, pending: true, backend, error: result.error };
217
+
218
+ await setAppleWebFlag(targetDir, true);
219
+ return { applied: true, pending: false, backend };
220
+ }
221
+
222
+ module.exports = { runAppleWeb, setAppleWebFlag, autoApplyAppleWebIfCached };
@@ -20,6 +20,7 @@ const kleur = require('kleur');
20
20
  const ui = require('../utils/ui');
21
21
  const { printCompactHeader } = require('../utils/brand');
22
22
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
23
+ const { readFacebookState, writeFacebookCredentials } = require('../utils/facebook');
23
24
 
24
25
  const PLACEHOLDER_REGEX = /^(YOUR_|TODO|CHANGE_ME|<.*>$)/i;
25
26
 
@@ -261,97 +262,8 @@ async function updateEnvFile(envPath, updates) {
261
262
  await fs.outputFile(envPath, updated.join('\n'), 'utf8');
262
263
  }
263
264
 
264
- // ── Facebook patches ──────────────────────────────────────────────────────
265
- // Facebook needs the same values in two build-time files: iOS Info.plist and
266
- // Android strings.xml. Both ship with placeholder zeros; we replace them in
267
- // place so the user only types each value once.
268
-
269
- function readFacebookCurrent(content, kind) {
270
- if (kind === 'plist') {
271
- const appId = content.match(/<key>FacebookAppID<\/key>\s*<string>([^<]*)<\/string>/);
272
- const token = content.match(/<key>FacebookClientToken<\/key>\s*<string>([^<]*)<\/string>/);
273
- return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
274
- }
275
- // strings.xml
276
- const appId = content.match(/<string name="facebook_app_id"[^>]*>([^<]*)<\/string>/);
277
- const token = content.match(/<string name="facebook_client_token"[^>]*>([^<]*)<\/string>/);
278
- return { appId: (appId?.[1] || '').trim(), token: (token?.[1] || '').trim() };
279
- }
280
-
281
- function isFacebookPlaceholder(value) {
282
- return !value || /^0+$/.test(value);
283
- }
284
-
285
- async function readFacebookState(projectDir) {
286
- const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
287
- const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
288
- const state = { appId: '', token: '', plistExists: false, stringsExists: false };
289
- if (await fs.pathExists(plistPath)) {
290
- state.plistExists = true;
291
- const plistRead = readFacebookCurrent(await fs.readFile(plistPath, 'utf8'), 'plist');
292
- if (!isFacebookPlaceholder(plistRead.appId)) state.appId = plistRead.appId;
293
- if (!isFacebookPlaceholder(plistRead.token)) state.token = plistRead.token;
294
- }
295
- if (await fs.pathExists(stringsPath)) {
296
- state.stringsExists = true;
297
- const stringsRead = readFacebookCurrent(await fs.readFile(stringsPath, 'utf8'), 'strings');
298
- // Prefer plist value when both exist; only fall back if plist was placeholder.
299
- if (!state.appId && !isFacebookPlaceholder(stringsRead.appId)) state.appId = stringsRead.appId;
300
- if (!state.token && !isFacebookPlaceholder(stringsRead.token)) state.token = stringsRead.token;
301
- }
302
- return state;
303
- }
304
-
305
- async function writeFacebookCredentials(projectDir, appId, clientToken) {
306
- const plistPath = path.join(projectDir, 'ios', 'Runner', 'Info.plist');
307
- const stringsPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'res', 'values', 'strings.xml');
308
- const results = { plist: 'skipped', strings: 'skipped' };
309
-
310
- if (appId && await fs.pathExists(plistPath)) {
311
- let plist = await fs.readFile(plistPath, 'utf8');
312
- plist = plist.replace(
313
- /(<key>FacebookAppID<\/key>\s*<string>)[^<]*(<\/string>)/,
314
- `$1${appId}$2`
315
- );
316
- if (clientToken) {
317
- plist = plist.replace(
318
- /(<key>FacebookClientToken<\/key>\s*<string>)[^<]*(<\/string>)/,
319
- `$1${clientToken}$2`
320
- );
321
- }
322
- // CFBundleURLSchemes — replace `fb<zeros>` with `fb<appId>` so deep links work.
323
- plist = plist.replace(/<string>fb0+<\/string>/, `<string>fb${appId}</string>`);
324
- await fs.outputFile(plistPath, plist, 'utf8');
325
- results.plist = 'ok';
326
- } else if (appId) {
327
- results.plist = 'missing_file';
328
- }
329
-
330
- if (appId && await fs.pathExists(stringsPath)) {
331
- let xml = await fs.readFile(stringsPath, 'utf8');
332
- xml = xml.replace(
333
- /(<string name="facebook_app_id"[^>]*>)[^<]*(<\/string>)/,
334
- `$1${appId}$2`
335
- );
336
- if (clientToken) {
337
- xml = xml.replace(
338
- /(<string name="facebook_client_token"[^>]*>)[^<]*(<\/string>)/,
339
- `$1${clientToken}$2`
340
- );
341
- }
342
- // fb_login_protocol_scheme — set if present.
343
- xml = xml.replace(
344
- /(<string name="fb_login_protocol_scheme"[^>]*>)[^<]*(<\/string>)/,
345
- `$1fb${appId}$2`
346
- );
347
- await fs.outputFile(stringsPath, xml, 'utf8');
348
- results.strings = 'ok';
349
- } else if (appId) {
350
- results.strings = 'missing_file';
351
- }
352
-
353
- return results;
354
- }
265
+ // Facebook native build-time files (Info.plist + strings.xml) read/write moved to
266
+ // lib/utils/facebook.js so `kasy configure` and `kasy facebook` share one source.
355
267
 
356
268
  async function setFirebaseSecret(projectDir, key, value, t) {
357
269
  // Use a tmp file to avoid newline injection / shell escaping issues.
@@ -151,6 +151,26 @@ async function runIosReleaseChecks(projectDir, t, language, config = {}) {
151
151
  ui.log.warn(`${t('doctor.ios.appleSignInEntitlementMissing')}\n${kleur.dim(appleEntitlement.error)}`);
152
152
  }
153
153
 
154
+ // Apple Sign-In on the WEB (Firebase/Supabase). Native iOS is covered above; the
155
+ // web button only ships once configured, so surface whether it's done or pending.
156
+ if (config.backendProvider === 'firebase' || config.backendProvider === 'supabase') {
157
+ let webConfigured = false;
158
+ try {
159
+ const featuresContent = await fs.readFile(
160
+ path.join(projectDir, 'lib', 'core', 'config', 'features.dart'),
161
+ 'utf8',
162
+ );
163
+ webConfigured = /const bool withAppleWebSignin\s*=\s*true\s*;/.test(featuresContent);
164
+ } catch {
165
+ // features.dart not found — leave as not configured.
166
+ }
167
+ if (webConfigured) {
168
+ ui.log.success(t('doctor.appleWeb.ok'));
169
+ } else {
170
+ ui.log.message(kleur.dim(`– ${t('doctor.appleWeb.pending')}`));
171
+ }
172
+ }
173
+
154
174
  if (config.withFacebookPixel) {
155
175
  const facebook = await validateFacebookInfoPlist(projectDir);
156
176
  if (facebook.skipped) {
@@ -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 };