kasy-cli 1.10.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/bin/kasy.js +25 -4
  2. package/lib/commands/check.js +40 -50
  3. package/lib/commands/deploy.js +25 -25
  4. package/lib/commands/splash.js +220 -0
  5. package/lib/scaffold/CHANGELOG.json +9 -0
  6. package/lib/scaffold/backends/api/patch/lib/main.dart +29 -10
  7. package/lib/scaffold/backends/supabase/patch/lib/main.dart +29 -10
  8. package/lib/scaffold/features/README.md +15 -139
  9. package/lib/scaffold/shared/generator-utils.js +16 -15
  10. package/lib/utils/i18n.js +292 -43
  11. package/package.json +2 -2
  12. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  13. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  14. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  15. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  16. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  17. package/templates/firebase/android/app/src/main/res/drawable-night/launch_background.xml +9 -0
  18. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  19. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  20. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-night-v21/launch_background.xml +9 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +2 -1
  37. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -0
  38. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  39. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  40. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +9 -8
  41. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  42. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +33 -0
  43. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  44. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  45. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  46. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  47. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  48. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  49. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  50. package/templates/firebase/lib/core/initializer/onstart_widget.dart +7 -1
  51. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +48 -24
  52. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +4 -0
  53. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +1 -0
  54. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +13 -0
  55. package/templates/firebase/lib/features/settings/settings_page.dart +158 -18
  56. package/templates/firebase/lib/i18n/en.i18n.json +6 -2
  57. package/templates/firebase/lib/i18n/es.i18n.json +6 -2
  58. package/templates/firebase/lib/i18n/pt.i18n.json +6 -2
  59. package/templates/firebase/lib/main.dart +29 -10
  60. package/templates/firebase/pubspec.yaml +4 -5
  61. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +1 -1
  62. package/templates/firebase/web/index.html +47 -39
  63. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  64. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  65. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  66. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  67. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  68. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  69. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  70. package/templates/firebase/web/splash/img/light-4x.png +0 -0
  71. package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +0 -124
  72. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +0 -35
  73. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +0 -35
  74. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +0 -35
  75. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +0 -12
  76. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +0 -12
  77. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +0 -12
  78. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +0 -17
  79. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +0 -17
  80. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +0 -17
  81. package/lib/scaffold/features/ci/.github/dependabot.yml +0 -16
  82. package/lib/scaffold/features/ci/.github/workflows/app.yml +0 -20
  83. package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +0 -14
  84. package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +0 -19
  85. package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +0 -163
  86. package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +0 -28
  87. package/lib/scaffold/features/ci/.gitlab-ci.yml +0 -37
  88. package/lib/scaffold/features/ci/codemagic.yaml +0 -157
  89. package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +0 -111
  90. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +0 -27
  91. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +0 -27
  92. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +0 -50
  93. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +0 -79
  94. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +0 -48
  95. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +0 -42
  96. package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +0 -147
  97. package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +0 -95
  98. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  99. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +0 -175
  100. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -76
  101. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +0 -279
  102. package/lib/scaffold/features/ios-release/.kasy/apple.env.example +0 -8
  103. package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +0 -7
  104. package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +0 -50
  105. package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +0 -50
  106. package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +0 -50
  107. package/lib/scaffold/features/ios-release/docs/ios-release.en.md +0 -41
  108. package/lib/scaffold/features/ios-release/docs/ios-release.es.md +0 -41
  109. package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +0 -41
  110. package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +0 -38
  111. package/lib/scaffold/features/ios-release/scripts/release-ios.sh +0 -137
  112. package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +0 -301
  113. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +0 -81
  114. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +0 -76
  115. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +0 -282
  116. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +0 -24
  117. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +0 -71
  118. package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +0 -92
  119. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +0 -15
  120. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +0 -78
  121. package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +0 -29
  122. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +0 -30
  123. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -66
  124. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +0 -72
  125. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +0 -92
  126. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  127. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +0 -89
  128. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +0 -94
  129. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +0 -80
  130. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +0 -139
  131. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +0 -110
  132. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +0 -84
  133. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -173
  134. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +0 -45
  135. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +0 -77
  136. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +0 -392
  137. package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +0 -116
  138. package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +0 -322
  139. package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +0 -41
  140. package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +0 -305
  141. package/templates/firebase/assets/images/splashscreen.png +0 -0
package/bin/kasy.js CHANGED
@@ -15,6 +15,7 @@ const { runRemove } = require('../lib/commands/remove');
15
15
  const { runUpdate } = require('../lib/commands/update');
16
16
  const { runDocs } = require('../lib/commands/docs');
17
17
  const { runNotificationsText } = require('../lib/commands/notifications');
18
+ const { runSplash } = require('../lib/commands/splash');
18
19
  const {
19
20
  runConfigure: runIosConfigure,
20
21
  runBuild: runIosBuild,
@@ -242,14 +243,15 @@ function buildProgram(language) {
242
243
  const langName = t('cli.command.setup.langName');
243
244
  applyLocalizedHelp(
244
245
  program
245
- .command('setup')
246
+ .command('setup', { hidden: true })
246
247
  .argument(`[${directoryName}]`, t('cli.command.setup.directoryArg'), '.')
247
248
  .option(`-l, --lang <${langName}>`, t('cli.command.setup.langOption'))
248
249
  .option('-b, --backend <backend>', t('cli.command.setup.backendOption'))
249
250
  .option('--with <features>', t('cli.command.setup.featuresOption'))
250
251
  .description(t('cli.command.setup.description'))
251
252
  .action(async (directory, options) => {
252
- // `setup` is an alias for `new` — unified flow
253
+ // `setup` is an alias for `new` — unified flow.
254
+ // Hidden from root help to reduce noise; still callable.
253
255
  await runNew(directory, {
254
256
  language: options.lang,
255
257
  backend: options.backend,
@@ -509,6 +511,26 @@ function buildProgram(language) {
509
511
  t
510
512
  );
511
513
 
514
+ applyLocalizedHelp(
515
+ program
516
+ .command('splash')
517
+ .argument('[directory]', 'Project folder (default: current directory)', '.')
518
+ .requiredOption('--light <path>', 'PNG with dark logo (shown in light mode)')
519
+ .requiredOption('--dark <path>', 'PNG with light logo (shown in dark mode)')
520
+ .option('--skip-generate', 'Only copy files, do not run flutter_native_splash:create', false)
521
+ .description(t('cli.command.splash.description'))
522
+ .action(async (directory, options) => {
523
+ const dir = directory || '.';
524
+ await runSplash(dir, {
525
+ language,
526
+ light: options.light,
527
+ dark: options.dark,
528
+ skipGenerate: options.skipGenerate,
529
+ });
530
+ }),
531
+ t
532
+ );
533
+
512
534
  const notificationsCmd = program
513
535
  .command('notifications')
514
536
  .description(t('cli.command.notifications.description'));
@@ -516,7 +538,6 @@ function buildProgram(language) {
516
538
  notificationsCmd
517
539
  .command('text')
518
540
  .argument('[directory]', 'Project folder (default: current directory)', '.')
519
- .option('-d, --directory <path>', 'Project folder (default: current directory)')
520
541
  .option('--locale <code>', 'i18n files to update: pt, en, es, or all (default: all)', 'all')
521
542
  .option('--demo-title <text>', 'Home → Features demo notification title')
522
543
  .option('--demo-body <text>', 'Home → Features demo card description / instant notification body')
@@ -524,7 +545,7 @@ function buildProgram(language) {
524
545
  .option('--reminder-body <text>', 'Scheduled reminder notification body')
525
546
  .description(t('cli.command.notifications.text.description'))
526
547
  .action(async (directory, options) => {
527
- const dir = options.directory || directory || '.';
548
+ const dir = directory || '.';
528
549
  await runNotificationsText(dir, {
529
550
  language,
530
551
  directory: dir,
@@ -110,67 +110,67 @@ async function runCheck(options = {}) {
110
110
  const t = createTranslator(lang);
111
111
 
112
112
  printCompactHeader(t);
113
- ui.intro('Kasy Check — Push Notifications');
113
+ ui.intro(t('check.intro'));
114
114
 
115
115
  // ── Detect backend ────────────────────────────────────────────────────────
116
116
  const isFirebase = await fs.pathExists(path.join(projectDir, 'firebase.json'));
117
117
  const isSupabase = await fs.pathExists(path.join(projectDir, 'supabase'));
118
118
 
119
119
  if (isFirebase && !isSupabase) {
120
- ok('Firebase backend');
121
- info('Firebase usa Application Default Credentials — nenhuma configuração extra necessária.');
120
+ ok(t('check.firebase.detected'));
121
+ info(t('check.firebase.adcInfo'));
122
122
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
123
123
  const apnsLines = [
124
- kleur.yellow('Push iOS requer APNs Key (não verificável via CLI)'),
125
- kleur.dim('Firebase Console → Cloud Messaging → app iOS → Chave de autenticação APNs'),
124
+ kleur.yellow(t('check.apns.warn')),
125
+ kleur.dim(t('check.apns.where')),
126
126
  ];
127
127
  if (firebaseProjectId) {
128
128
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
129
129
  }
130
130
  ui.note(apnsLines.join('\n'));
131
- ui.outro('Done');
131
+ ui.outro(t('check.done'));
132
132
  return;
133
133
  }
134
134
 
135
135
  if (!isSupabase) {
136
- ui.log.error('Diretório não parece ser um projeto Kasy.');
137
- ui.cancel('Aborted');
136
+ ui.log.error(t('check.notKasy'));
137
+ ui.cancel(t('check.aborted'));
138
138
  process.exit(1);
139
139
  }
140
140
 
141
141
  // ── 1. Project linked? ────────────────────────────────────────────────────
142
142
  const projectRef = await readProjectRef(projectDir);
143
143
  if (!projectRef) {
144
- fail('Projeto Supabase não vinculado', 'execute: supabase link --project-ref SEU_REF');
145
- ui.cancel('Aborted');
144
+ fail(t('check.supabase.notLinked'), t('check.supabase.notLinkedHint'));
145
+ ui.cancel(t('check.aborted'));
146
146
  process.exit(1);
147
147
  }
148
- ok('Projeto vinculado', projectRef);
148
+ ok(t('check.supabase.linked'), projectRef);
149
149
 
150
150
  // ── Read secrets list ─────────────────────────────────────────────────────
151
151
  const spinner = ui.spinner();
152
- spinner.start('Verificando secrets');
152
+ spinner.start(t('check.spin.secrets'));
153
153
  const secrets = await listSecretNames(projectDir);
154
- spinner.stop('Secrets verificados');
154
+ spinner.stop(t('check.spin.secretsDone'));
155
155
 
156
156
  if (!secrets) {
157
- warn('Não foi possível listar secrets', 'verifique: supabase login');
157
+ warn(t('check.secrets.listFailed'), t('check.secrets.checkLogin'));
158
158
  }
159
159
 
160
160
  // ── 2. FIREBASE_PROJECT_ID ────────────────────────────────────────────────
161
161
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
162
162
  if (secrets) {
163
163
  if (secrets.has('FIREBASE_PROJECT_ID')) {
164
- ok('FIREBASE_PROJECT_ID configurado');
164
+ ok(t('check.fbProjId.ok'));
165
165
  } else {
166
- fail('FIREBASE_PROJECT_ID ausente');
166
+ fail(t('check.fbProjId.missing'));
167
167
  if (firebaseProjectId) {
168
- ui.log.message(kleur.gray(`Corrija: supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`));
168
+ ui.log.message(kleur.gray(t('check.fbProjId.fixHint', { id: firebaseProjectId })));
169
169
  if (options.fix) {
170
170
  const r = await runCmd(`supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`, projectDir);
171
171
  r.ok
172
- ? ok('FIREBASE_PROJECT_ID → configurado automaticamente')
173
- : fail('FIREBASE_PROJECT_ID → falhou ao configurar', r.error);
172
+ ? ok(t('check.fbProjId.fixed'))
173
+ : fail(t('check.fbProjId.fixFailed'), r.error);
174
174
  }
175
175
  }
176
176
  }
@@ -179,15 +179,15 @@ async function runCheck(options = {}) {
179
179
  // ── 3. FIREBASE_SERVICE_ACCOUNT_JSON ─────────────────────────────────────
180
180
  if (secrets) {
181
181
  if (secrets.has('FIREBASE_SERVICE_ACCOUNT_JSON')) {
182
- ok('FIREBASE_SERVICE_ACCOUNT_JSON configurado');
182
+ ok(t('check.fbSak.ok'));
183
183
  } else {
184
- fail('FIREBASE_SERVICE_ACCOUNT_JSON ausente');
184
+ fail(t('check.fbSak.missing'));
185
185
 
186
186
  if (options.fix && firebaseProjectId) {
187
187
  const fixSpinner = ui.spinner();
188
- fixSpinner.start('Gerando e configurando chave FCM…');
188
+ fixSpinner.start(t('check.fbSak.spin'));
189
189
  const fcmResult = await createFcmServiceAccountKey(firebaseProjectId);
190
- fixSpinner.stop('Chave FCM gerada');
190
+ fixSpinner.stop(t('check.fbSak.spinDone'));
191
191
 
192
192
  if (fcmResult.ok) {
193
193
  const secretSteps = await setSupabaseSecrets(projectDir, {
@@ -196,62 +196,52 @@ async function runCheck(options = {}) {
196
196
  });
197
197
  const setStep = secretSteps.find((s) => s.name === 'secret FIREBASE_SERVICE_ACCOUNT_JSON');
198
198
  setStep?.ok
199
- ? ok('FIREBASE_SERVICE_ACCOUNT_JSON → configurado automaticamente')
200
- : fail('FIREBASE_SERVICE_ACCOUNT_JSON → falhou ao configurar', setStep?.error);
199
+ ? ok(t('check.fbSak.fixed'))
200
+ : fail(t('check.fbSak.fixFailed'), setStep?.error);
201
201
  } else {
202
- fail('FIREBASE_SERVICE_ACCOUNT_JSON → não foi possível gerar chave', fcmResult.error);
203
- ui.log.message(kleur.gray(
204
- 'Manual: Firebase Console → Configurações → Contas de serviço → Gerar chave\n' +
205
- "Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'"
206
- ));
202
+ fail(t('check.fbSak.genFailed'), fcmResult.error);
203
+ ui.log.message(kleur.gray(t('check.fbSak.manual')));
207
204
  }
208
205
  } else {
209
- ui.log.message(kleur.gray(
210
- 'Corrija automaticamente: kasy check --fix\n' +
211
- 'Ou manualmente: Firebase Console → Configurações → Contas de serviço → Gerar chave\n' +
212
- "Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'"
213
- ));
206
+ ui.log.message(kleur.gray(t('check.fbSak.hint')));
214
207
  }
215
208
  }
216
209
  }
217
210
 
218
211
  // ── 4. Edge function send-push-notification ────────────────────────────────
219
212
  const fnSpinner = ui.spinner();
220
- fnSpinner.start('Verificando edge functions…');
213
+ fnSpinner.start(t('check.fn.spin'));
221
214
  const functions = await listDeployedFunctions(projectDir);
222
- fnSpinner.stop('Edge functions verificadas');
215
+ fnSpinner.stop(t('check.fn.spinDone'));
223
216
 
224
217
  if (!functions) {
225
- warn('Não foi possível listar edge functions', 'verifique: supabase login');
218
+ warn(t('check.fn.listFailed'), t('check.secrets.checkLogin'));
226
219
  } else if (functions.has('send-push-notification')) {
227
- ok('Edge function send-push-notification deployada');
220
+ ok(t('check.fn.deployed'));
228
221
  } else {
229
- fail('Edge function send-push-notification não deployada');
222
+ fail(t('check.fn.missing'));
230
223
  if (options.fix) {
231
224
  const deploySpinner = ui.spinner();
232
- deploySpinner.start('Publicando send-push-notification…');
225
+ deploySpinner.start(t('check.fn.deploySpin'));
233
226
  const r = await runCmd('supabase functions deploy send-push-notification', projectDir);
234
227
  r.ok
235
- ? deploySpinner.stop('send-push-notification → deployada automaticamente')
236
- : deploySpinner.error(`send-push-notification → falhou no deploy${r.error ? ` — ${r.error}` : ''}`);
228
+ ? deploySpinner.stop(t('check.fn.deployDone'))
229
+ : deploySpinner.error(`${t('check.fn.deployFailed')}${r.error ? ` — ${r.error}` : ''}`);
237
230
  } else {
238
- ui.log.message(kleur.gray(
239
- 'Corrija automaticamente: kasy check --fix\n' +
240
- 'Ou manualmente: supabase functions deploy send-push-notification'
241
- ));
231
+ ui.log.message(kleur.gray(t('check.fn.hint')));
242
232
  }
243
233
  }
244
234
 
245
235
  // ── 5. APNs reminder ─────────────────────────────────────────────────────
246
236
  const apnsLines = [
247
- kleur.yellow('Push iOS requer APNs Key (não verificável via CLI)'),
248
- kleur.dim('Firebase Console → Cloud Messaging → app iOS → Chave de autenticação APNs'),
237
+ kleur.yellow(t('check.apns.warn')),
238
+ kleur.dim(t('check.apns.where')),
249
239
  ];
250
240
  if (firebaseProjectId) {
251
241
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
252
242
  }
253
243
  ui.note(apnsLines.join('\n'));
254
- ui.outro('Done');
244
+ ui.outro(t('check.done'));
255
245
  }
256
246
 
257
247
  module.exports = { runCheck };
@@ -123,14 +123,14 @@ async function deployFirebase(projectDir, options, tr) {
123
123
  }
124
124
 
125
125
  const spinner = ui.spinner();
126
- spinner.start('Deploying Firebase...');
126
+ spinner.start(tr('deploy.firebase.spin'));
127
127
  let steps;
128
128
  try {
129
129
  steps = await runFirebaseDeploy(projectDir, null, firebaseProjectId, {
130
130
  functionsRegion,
131
131
  onProgress: (key) => { spinner.message(String(key)); },
132
132
  });
133
- spinner.stop('Firebase deploy completed');
133
+ spinner.stop(tr('deploy.firebase.spinDone'));
134
134
  } catch (err) {
135
135
  spinner.error(err.message);
136
136
  throw err;
@@ -145,9 +145,9 @@ async function deployFirebase(projectDir, options, tr) {
145
145
  // ── APNs reminder — required for iOS push notifications ──────────────────
146
146
  // Cannot be automated: the .p8 key only exists in Apple Developer Portal.
147
147
  const apnsBody = [
148
- kleur.yellow('Push iOS: configure a APNs Key no Firebase Console'),
149
- kleur.dim('1. Apple Developer Portal → Keys → criar APNs Key (.p8)'),
150
- kleur.dim('2. Firebase Console → Cloud Messaging → app iOS → upload da APNs Key'),
148
+ kleur.yellow(tr('deploy.apns.title')),
149
+ kleur.dim(tr('deploy.apns.step1')),
150
+ kleur.dim(tr('deploy.apns.step2')),
151
151
  kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`),
152
152
  ].join('\n');
153
153
  ui.note(apnsBody);
@@ -155,28 +155,28 @@ async function deployFirebase(projectDir, options, tr) {
155
155
 
156
156
  // ── Supabase deploy ───────────────────────────────────────────────────────────
157
157
 
158
- async function deploySupabase(projectDir) {
158
+ async function deploySupabase(projectDir, tr) {
159
159
  // ── 1. Get project ref ──────────────────────────────────────────────────
160
160
  const projectRef = await readSupabaseProjectRef(projectDir);
161
161
  if (!projectRef) {
162
- ui.log.error('Projeto Supabase não está vinculado neste diretório.');
163
- ui.log.message(kleur.gray('Execute: supabase link --project-ref SEU_PROJECT_REF'));
162
+ ui.log.error(tr('deploy.supabase.notLinked'));
163
+ ui.log.message(kleur.gray(tr('deploy.supabase.linkHint')));
164
164
  process.exit(1);
165
165
  }
166
- ui.log.message(kleur.gray(`Project ref: ${kleur.cyan(projectRef)}`));
166
+ ui.log.message(kleur.gray(tr('deploy.supabase.projectRef', { ref: kleur.cyan(projectRef) })));
167
167
 
168
168
  // ── 2. FCM Service Account JSON ─────────────────────────────────────────
169
169
  const alreadySet = await isServiceAccountJsonSet(projectDir);
170
170
  if (alreadySet) {
171
- printStep(true, false, 'FIREBASE_SERVICE_ACCOUNT_JSON já configurado');
171
+ printStep(true, false, tr('deploy.supabase.sakAlready'));
172
172
  } else {
173
173
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
174
174
 
175
175
  if (firebaseProjectId) {
176
176
  const fcmSpinner = ui.spinner();
177
- fcmSpinner.start('Gerando chave FCM (Service Account)…');
177
+ fcmSpinner.start(tr('deploy.supabase.sakSpin'));
178
178
  const fcmResult = await createFcmServiceAccountKey(firebaseProjectId);
179
- fcmSpinner.stop('Chave FCM gerada');
179
+ fcmSpinner.stop(tr('deploy.supabase.sakSpinDone'));
180
180
 
181
181
  if (fcmResult.ok) {
182
182
  const secretSteps = await setSupabaseSecrets(projectDir, {
@@ -186,23 +186,23 @@ async function deploySupabase(projectDir) {
186
186
  for (const s of secretSteps) printStep(s.ok, false, s.name, s.error);
187
187
  } else {
188
188
  printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', fcmResult.error);
189
- ui.log.warn("Configure manualmente: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='...'");
189
+ ui.log.warn(tr('deploy.supabase.sakManual'));
190
190
  }
191
191
  } else {
192
- printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', 'google-services.json não encontrado — configure manualmente');
192
+ printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', tr('deploy.supabase.sakNoGS'));
193
193
  }
194
194
  }
195
195
 
196
196
  // ── 3. Deploy edge functions ────────────────────────────────────────────
197
197
  const fnSpinner = ui.spinner();
198
- fnSpinner.start('Publicando edge functions…');
198
+ fnSpinner.start(tr('deploy.supabase.fnSpin'));
199
199
  const fnResult = await deployFunctions(projectDir);
200
- fnSpinner.stop('Edge functions processadas');
200
+ fnSpinner.stop(tr('deploy.supabase.fnSpinDone'));
201
201
 
202
202
  if (Array.isArray(fnResult)) {
203
203
  fnResult.forEach((s) => printStep(s.ok, s.skipped, s.name, s.error));
204
204
  } else if (fnResult.skipped) {
205
- printStep(true, true, 'edge functions (nenhuma encontrada)');
205
+ printStep(true, true, tr('deploy.supabase.fnNone'));
206
206
  } else {
207
207
  printStep(fnResult.ok, false, 'supabase functions deploy', fnResult.error);
208
208
  }
@@ -210,9 +210,9 @@ async function deploySupabase(projectDir) {
210
210
  // ── 4. APNs reminder ────────────────────────────────────────────────────
211
211
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
212
212
  const apnsLines = [
213
- kleur.yellow('Push iOS: configure a APNs Key no Firebase Console'),
214
- kleur.dim('1. Apple Developer Portal → Keys → criar APNs Key (.p8)'),
215
- kleur.dim('2. Firebase Console → Cloud Messaging → app iOS → upload da APNs Key'),
213
+ kleur.yellow(tr('deploy.apns.title')),
214
+ kleur.dim(tr('deploy.apns.step1')),
215
+ kleur.dim(tr('deploy.apns.step2')),
216
216
  ];
217
217
  if (firebaseProjectId) {
218
218
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
@@ -240,17 +240,17 @@ async function runDeployCommand(directory, options = {}, { language: langHint }
240
240
 
241
241
  if (isFirebase) {
242
242
  printCompactHeader(tr);
243
- ui.intro('Deploy — Firebase');
243
+ ui.intro(tr('deploy.firebase.intro'));
244
244
  await deployFirebase(projectDir, options, tr);
245
- ui.outro('Deploy concluído');
245
+ ui.outro(tr('deploy.outro'));
246
246
  return;
247
247
  }
248
248
 
249
249
  if (isSupabase) {
250
250
  printCompactHeader(tr);
251
- ui.intro('Deploy — Supabase');
252
- await deploySupabase(projectDir);
253
- ui.outro('Deploy concluído');
251
+ ui.intro(tr('deploy.supabase.intro'));
252
+ await deploySupabase(projectDir, tr);
253
+ ui.outro(tr('deploy.outro'));
254
254
  return;
255
255
  }
256
256
 
@@ -0,0 +1,220 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const fsp = require('node:fs/promises');
4
+ const { exec } = require('node:child_process');
5
+ const { promisify } = require('node:util');
6
+ const kleur = require('kleur');
7
+ const ui = require('../utils/ui');
8
+ const { printCompactHeader } = require('../utils/brand');
9
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ const ASSETS_DIR = path.join('assets', 'images');
14
+ const LIGHT_NAME = 'splash_logo_light.png';
15
+ const DARK_NAME = 'splash_logo_dark.png';
16
+
17
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
18
+
19
+ /**
20
+ * Parse the PNG IHDR chunk to read color type and detect transparency.
21
+ *
22
+ * PNG layout: 8-byte signature + chunks. The IHDR chunk is always first,
23
+ * placed at byte offset 8. It holds: length(4) + "IHDR"(4) + data(13) + crc(4).
24
+ * Color type is the 10th byte of data → absolute offset 25.
25
+ *
26
+ * Color types with alpha or transparency support:
27
+ * 4 = grayscale + alpha
28
+ * 6 = truecolor + alpha
29
+ * 3 = indexed (transparency only if a tRNS chunk is present)
30
+ *
31
+ * @param {string} filePath
32
+ * @returns {Promise<{ valid: boolean, hasAlpha: boolean, colorType: number, width: number, height: number }>}
33
+ */
34
+ async function inspectPng(filePath) {
35
+ const handle = await fsp.open(filePath, 'r');
36
+ try {
37
+ const header = Buffer.alloc(33);
38
+ await handle.read(header, 0, 33, 0);
39
+ const sig = header.subarray(0, 8);
40
+ if (!sig.equals(PNG_SIGNATURE)) {
41
+ return { valid: false, hasAlpha: false, colorType: -1, width: 0, height: 0 };
42
+ }
43
+ const width = header.readUInt32BE(16);
44
+ const height = header.readUInt32BE(20);
45
+ const colorType = header.readUInt8(25);
46
+
47
+ let hasAlpha = colorType === 4 || colorType === 6;
48
+
49
+ if (!hasAlpha && colorType === 3) {
50
+ const { size } = await handle.stat();
51
+ const remaining = Math.min(size - 33, 256 * 1024);
52
+ if (remaining > 0) {
53
+ const tail = Buffer.alloc(remaining);
54
+ await handle.read(tail, 0, remaining, 33);
55
+ if (tail.includes(Buffer.from('tRNS'))) {
56
+ hasAlpha = true;
57
+ }
58
+ }
59
+ }
60
+
61
+ return { valid: true, hasAlpha, colorType, width, height };
62
+ } finally {
63
+ await handle.close();
64
+ }
65
+ }
66
+
67
+ async function assertKasyProject(projectDir, t) {
68
+ const kitSetupPath = path.join(projectDir, 'kit_setup.json');
69
+ const pubspecPath = path.join(projectDir, 'pubspec.yaml');
70
+ if (!(await fs.pathExists(kitSetupPath)) && !(await fs.pathExists(pubspecPath))) {
71
+ throw new Error(t('splash.error.notKasyProject'));
72
+ }
73
+ const assetsDir = path.join(projectDir, ASSETS_DIR);
74
+ await fs.ensureDir(assetsDir);
75
+ }
76
+
77
+ /**
78
+ * @param {string} flagValue
79
+ * @param {string} role 'light' | 'dark'
80
+ */
81
+ function resolveInputPath(flagValue, role) {
82
+ if (!flagValue) return null;
83
+ const expanded = flagValue.startsWith('~')
84
+ ? path.join(require('node:os').homedir(), flagValue.slice(1))
85
+ : flagValue;
86
+ return path.resolve(expanded);
87
+ }
88
+
89
+ /**
90
+ * @param {string} projectDir
91
+ * @param {{ light?: string, dark?: string, skipGenerate?: boolean, language?: string }} options
92
+ */
93
+ async function runSplash(projectDir, options = {}) {
94
+ const language = options.language || detectDefaultLanguage();
95
+ const t = createTranslator(language);
96
+
97
+ printCompactHeader();
98
+ ui.intro(kleur.bold().cyan(t('splash.intro')));
99
+
100
+ await assertKasyProject(projectDir, t);
101
+
102
+ const lightPath = resolveInputPath(options.light, 'light');
103
+ const darkPath = resolveInputPath(options.dark, 'dark');
104
+
105
+ if (!lightPath || !darkPath) {
106
+ ui.log.error(t('splash.error.bothRequired'));
107
+ ui.log.message(kleur.dim('kasy splash --light <light.png> --dark <dark.png>'));
108
+ process.exit(1);
109
+ }
110
+
111
+ for (const [role, p] of [['light', lightPath], ['dark', darkPath]]) {
112
+ if (!(await fs.pathExists(p))) {
113
+ ui.log.error(t('splash.error.fileNotFound', { path: p }));
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ const inspectSpinner = ui.spinner();
119
+ inspectSpinner.start(t('splash.validating'));
120
+
121
+ let lightInfo;
122
+ let darkInfo;
123
+ try {
124
+ lightInfo = await inspectPng(lightPath);
125
+ darkInfo = await inspectPng(darkPath);
126
+ } catch (err) {
127
+ inspectSpinner.stop(`✖ ${err.message || t('splash.error.notPng')}`);
128
+ process.exit(1);
129
+ }
130
+
131
+ if (!lightInfo.valid || !darkInfo.valid) {
132
+ inspectSpinner.stop(`✖ ${t('splash.error.notPng')}`);
133
+ process.exit(1);
134
+ }
135
+
136
+ inspectSpinner.stop(t('splash.validated'));
137
+
138
+ const warnings = [];
139
+ if (!lightInfo.hasAlpha) {
140
+ warnings.push(t('splash.warn.noAlphaLight', { path: path.basename(lightPath) }));
141
+ }
142
+ if (!darkInfo.hasAlpha) {
143
+ warnings.push(t('splash.warn.noAlphaDark', { path: path.basename(darkPath) }));
144
+ }
145
+ if (lightInfo.width < 768 || lightInfo.height < 768) {
146
+ warnings.push(t('splash.warn.smallLight', {
147
+ w: lightInfo.width,
148
+ h: lightInfo.height,
149
+ }));
150
+ }
151
+ if (darkInfo.width < 768 || darkInfo.height < 768) {
152
+ warnings.push(t('splash.warn.smallDark', {
153
+ w: darkInfo.width,
154
+ h: darkInfo.height,
155
+ }));
156
+ }
157
+ if (warnings.length > 0) {
158
+ ui.note(warnings.map((w) => `${kleur.yellow('⚠')} ${w}`).join('\n'), t('splash.warn.title'));
159
+ }
160
+
161
+ const destLight = path.join(projectDir, ASSETS_DIR, LIGHT_NAME);
162
+ const destDark = path.join(projectDir, ASSETS_DIR, DARK_NAME);
163
+
164
+ const copySpinner = ui.spinner();
165
+ copySpinner.start(t('splash.copying'));
166
+ await fs.copy(lightPath, destLight, { overwrite: true });
167
+ await fs.copy(darkPath, destDark, { overwrite: true });
168
+ copySpinner.stop(t('splash.copied'));
169
+
170
+ if (options.skipGenerate) {
171
+ ui.note(t('splash.skipGenerate.hint'), t('splash.skipGenerate.title'));
172
+ ui.outro(t('splash.done'));
173
+ return;
174
+ }
175
+
176
+ const genSpinner = ui.spinner();
177
+ genSpinner.start(t('splash.generating'));
178
+ const result = await runFlutterNativeSplash(projectDir);
179
+ if (result.ok) {
180
+ genSpinner.stop(t('splash.generated'));
181
+ } else {
182
+ genSpinner.stop(`⚠ ${t('splash.error.generateFailed')}`);
183
+ if (result.stderr) {
184
+ ui.log.message(kleur.dim(result.stderr.split('\n').slice(0, 8).join('\n')));
185
+ }
186
+ ui.log.message(kleur.dim('dart run flutter_native_splash:create'));
187
+ process.exit(1);
188
+ }
189
+
190
+ const summary = [
191
+ `${kleur.bold(t('splash.summary.light'))}: ${kleur.white(LIGHT_NAME)} (${lightInfo.width}x${lightInfo.height})`,
192
+ `${kleur.bold(t('splash.summary.dark'))}: ${kleur.white(DARK_NAME)} (${darkInfo.width}x${darkInfo.height})`,
193
+ ].join('\n');
194
+ ui.note(summary, t('splash.summary.title'));
195
+
196
+ ui.outro(t('splash.done'));
197
+ }
198
+
199
+ async function runFlutterNativeSplash(projectDir) {
200
+ try {
201
+ const { stdout, stderr } = await execAsync(
202
+ 'dart run flutter_native_splash:create',
203
+ {
204
+ cwd: projectDir,
205
+ maxBuffer: 16 * 1024 * 1024,
206
+ timeout: 240_000,
207
+ },
208
+ );
209
+ return { ok: true, stdout, stderr };
210
+ } catch (err) {
211
+ return {
212
+ ok: false,
213
+ error: err.message,
214
+ stdout: err.stdout || '',
215
+ stderr: err.stderr || '',
216
+ };
217
+ }
218
+ }
219
+
220
+ module.exports = { runSplash, inspectPng };
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.13.0": {
3
+ "modules": {
4
+ "onboarding": {
5
+ "pt": "Botão \"Já tem conta? Entrar\" na primeira tela do onboarding — quem já tem conta entra direto sem passar pelo fluxo todo, sem precisar criar usuário anônimo antes",
6
+ "en": "\"Already have an account? Log in\" button on the first onboarding screen — returning users go straight to sign-in instead of going through the full flow as an anonymous user first",
7
+ "es": "Botón \"¿Ya tienes cuenta? Iniciar sesión\" en la primera pantalla del onboarding — quien ya tiene cuenta entra directo sin pasar por todo el flujo como usuario anónimo"
8
+ }
9
+ }
10
+ },
2
11
  "1.10.0": {
3
12
  "modules": {
4
13
  "widget": {