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
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,7 +8,10 @@ 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');
13
+ const { runAppleWeb } = require('../lib/commands/apple-web');
14
+ const { runFacebook } = require('../lib/commands/facebook');
12
15
  const { runCheck } = require('../lib/commands/check');
13
16
  const { runRun } = require('../lib/commands/run');
14
17
  const { runReset } = require('../lib/commands/reset');
@@ -325,6 +328,27 @@ function buildProgram(language) {
325
328
  t
326
329
  );
327
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
+
328
352
  applyLocalizedHelp(
329
353
  program
330
354
  .command('configure')
@@ -336,6 +360,46 @@ function buildProgram(language) {
336
360
  t
337
361
  );
338
362
 
363
+ applyLocalizedHelp(
364
+ program
365
+ .command('apple-web')
366
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
367
+ .option('--service-id <id>', 'Apple Service ID (the OAuth client_id), e.g. com.acme.app.signin')
368
+ .option('--team-id <id>', 'Apple Developer Team ID')
369
+ .option('--key-id <id>', 'Key ID of the .p8')
370
+ .option('--p8 <path>', 'Path to the Sign In with Apple .p8 private key')
371
+ .description(t('cli.command.appleWeb.description'))
372
+ .action(async (directory, options) => {
373
+ await runAppleWeb(directory, {
374
+ language,
375
+ serviceId: options.serviceId,
376
+ teamId: options.teamId,
377
+ keyId: options.keyId,
378
+ p8: options.p8,
379
+ });
380
+ }),
381
+ t
382
+ );
383
+
384
+ applyLocalizedHelp(
385
+ program
386
+ .command('facebook')
387
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
388
+ .option('--app-id <id>', 'Meta App ID')
389
+ .option('--client-token <token>', 'Meta Client Token (Settings → Advanced)')
390
+ .option('--app-secret <secret>', 'Meta App Secret (for the Firebase/Supabase provider)')
391
+ .description(t('cli.command.facebook.description'))
392
+ .action(async (directory, options) => {
393
+ await runFacebook(directory, {
394
+ language,
395
+ appId: options.appId,
396
+ clientToken: options.clientToken,
397
+ appSecret: options.appSecret,
398
+ });
399
+ }),
400
+ t
401
+ );
402
+
339
403
  applyLocalizedHelp(
340
404
  program
341
405
  .command('check')
@@ -390,7 +454,7 @@ function buildProgram(language) {
390
454
  applyLocalizedHelp(
391
455
  program
392
456
  .command('add')
393
- .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...)')
394
458
  .option('--list', 'List all available features and their current status')
395
459
  .option('--yes', 'Skip interactive prompts (use placeholder values)')
396
460
  .option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
@@ -404,7 +468,7 @@ function buildProgram(language) {
404
468
  applyLocalizedHelp(
405
469
  program
406
470
  .command('remove')
407
- .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...)')
408
472
  .option('--yes', 'Skip confirmation prompt')
409
473
  .option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
410
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 |
@@ -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) {