kasy-cli 1.12.1 → 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 (138) hide show
  1. package/bin/kasy.js +21 -0
  2. package/lib/commands/splash.js +220 -0
  3. package/lib/scaffold/CHANGELOG.json +9 -0
  4. package/lib/scaffold/backends/api/patch/lib/main.dart +29 -10
  5. package/lib/scaffold/backends/supabase/patch/lib/main.dart +29 -10
  6. package/lib/scaffold/features/README.md +15 -139
  7. package/lib/scaffold/shared/generator-utils.js +16 -15
  8. package/lib/utils/i18n.js +78 -0
  9. package/package.json +2 -2
  10. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  11. package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
  12. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  13. package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
  14. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  15. package/templates/firebase/android/app/src/main/res/drawable-night/launch_background.xml +9 -0
  16. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  17. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
  18. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  19. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
  20. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-night-v21/launch_background.xml +9 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +2 -1
  35. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -0
  36. package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
  37. package/templates/firebase/assets/images/splash_logo_light.png +0 -0
  38. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +9 -8
  39. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  40. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +33 -0
  41. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  42. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  43. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  44. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
  45. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
  46. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
  47. package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
  48. package/templates/firebase/lib/core/theme/providers/theme_provider.dart +48 -24
  49. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +4 -0
  50. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +1 -0
  51. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +13 -0
  52. package/templates/firebase/lib/features/settings/settings_page.dart +158 -18
  53. package/templates/firebase/lib/i18n/en.i18n.json +6 -2
  54. package/templates/firebase/lib/i18n/es.i18n.json +6 -2
  55. package/templates/firebase/lib/i18n/pt.i18n.json +6 -2
  56. package/templates/firebase/lib/main.dart +29 -10
  57. package/templates/firebase/pubspec.yaml +4 -5
  58. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +1 -1
  59. package/templates/firebase/web/index.html +47 -39
  60. package/templates/firebase/web/splash/img/dark-1x.png +0 -0
  61. package/templates/firebase/web/splash/img/dark-2x.png +0 -0
  62. package/templates/firebase/web/splash/img/dark-3x.png +0 -0
  63. package/templates/firebase/web/splash/img/dark-4x.png +0 -0
  64. package/templates/firebase/web/splash/img/light-1x.png +0 -0
  65. package/templates/firebase/web/splash/img/light-2x.png +0 -0
  66. package/templates/firebase/web/splash/img/light-3x.png +0 -0
  67. package/templates/firebase/web/splash/img/light-4x.png +0 -0
  68. package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +0 -124
  69. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +0 -35
  70. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +0 -35
  71. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +0 -35
  72. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +0 -12
  73. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +0 -12
  74. package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +0 -12
  75. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +0 -17
  76. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +0 -17
  77. package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +0 -17
  78. package/lib/scaffold/features/ci/.github/dependabot.yml +0 -16
  79. package/lib/scaffold/features/ci/.github/workflows/app.yml +0 -20
  80. package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +0 -14
  81. package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +0 -19
  82. package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +0 -163
  83. package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +0 -28
  84. package/lib/scaffold/features/ci/.gitlab-ci.yml +0 -37
  85. package/lib/scaffold/features/ci/codemagic.yaml +0 -157
  86. package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +0 -111
  87. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +0 -27
  88. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +0 -27
  89. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +0 -50
  90. package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +0 -79
  91. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +0 -48
  92. package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +0 -42
  93. package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +0 -147
  94. package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +0 -95
  95. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  96. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +0 -175
  97. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -76
  98. package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +0 -279
  99. package/lib/scaffold/features/ios-release/.kasy/apple.env.example +0 -8
  100. package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +0 -7
  101. package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +0 -50
  102. package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +0 -50
  103. package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +0 -50
  104. package/lib/scaffold/features/ios-release/docs/ios-release.en.md +0 -41
  105. package/lib/scaffold/features/ios-release/docs/ios-release.es.md +0 -41
  106. package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +0 -41
  107. package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +0 -38
  108. package/lib/scaffold/features/ios-release/scripts/release-ios.sh +0 -137
  109. package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +0 -301
  110. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +0 -81
  111. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +0 -76
  112. package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +0 -282
  113. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +0 -24
  114. package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +0 -71
  115. package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +0 -92
  116. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +0 -15
  117. package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +0 -78
  118. package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +0 -29
  119. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +0 -30
  120. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -66
  121. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +0 -72
  122. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +0 -92
  123. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  124. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +0 -89
  125. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +0 -94
  126. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +0 -80
  127. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +0 -139
  128. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +0 -110
  129. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +0 -84
  130. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -173
  131. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +0 -45
  132. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +0 -77
  133. package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +0 -392
  134. package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +0 -116
  135. package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +0 -322
  136. package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +0 -41
  137. package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +0 -305
  138. 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,
@@ -510,6 +511,26 @@ function buildProgram(language) {
510
511
  t
511
512
  );
512
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
+
513
534
  const notificationsCmd = program
514
535
  .command('notifications')
515
536
  .description(t('cli.command.notifications.description'));
@@ -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": {
@@ -108,7 +108,7 @@ void run(SharedPreferences prefs) => runApp(
108
108
  // mode: ThemeMode.dark,
109
109
  // ),
110
110
  // See ./docs/theme.md for more details
111
- class MyApp extends ConsumerWidget {
111
+ class MyApp extends ConsumerStatefulWidget {
112
112
  final SharedPreferences sharedPreferences;
113
113
 
114
114
  const MyApp({
@@ -116,23 +116,42 @@ class MyApp extends ConsumerWidget {
116
116
  required this.sharedPreferences,
117
117
  });
118
118
 
119
+ @override
120
+ ConsumerState<MyApp> createState() => _MyAppState();
121
+ }
122
+
123
+ class _MyAppState extends ConsumerState<MyApp> {
124
+ late final AppTheme _appTheme;
125
+
126
+ @override
127
+ void initState() {
128
+ super.initState();
129
+ _appTheme = AppTheme.uniform(
130
+ sharedPreferences: widget.sharedPreferences,
131
+ themeFactory: const UniversalThemeFactory(),
132
+ lightColors: KasyColors.light(),
133
+ darkColors: KasyColors.dark(),
134
+ textTheme: KasyTextTheme.build(),
135
+ defaultMode: ThemeMode.system,
136
+ );
137
+ }
138
+
139
+ @override
140
+ void dispose() {
141
+ _appTheme.dispose();
142
+ super.dispose();
143
+ }
144
+
119
145
  // This widget is the root of your application.
120
146
  @override
121
- Widget build(BuildContext context, WidgetRef ref) {
147
+ Widget build(BuildContext context) {
122
148
  ErrorWidget.builder = (FlutterErrorDetails details) {
123
149
  return AppErrorWidget(error: details);
124
150
  };
125
151
  final goRouter = ref.watch(goRouterProvider);
126
152
 
127
153
  return ThemeProvider(
128
- notifier: AppTheme.uniform(
129
- sharedPreferences: sharedPreferences,
130
- themeFactory: const UniversalThemeFactory(),
131
- lightColors: KasyColors.light(),
132
- darkColors: KasyColors.dark(),
133
- textTheme: KasyTextTheme.build(),
134
- defaultMode: ThemeMode.light,
135
- ),
154
+ notifier: _appTheme,
136
155
  child: Builder(builder: (context) {
137
156
  return WebDevicePreview.wrap(
138
157
  child: DevInspector.wrap(
@@ -138,7 +138,7 @@ void run(SharedPreferences prefs) => runApp(
138
138
  // mode: ThemeMode.dark,
139
139
  // ),
140
140
  // See ./docs/theme.md for more details
141
- class MyApp extends ConsumerWidget {
141
+ class MyApp extends ConsumerStatefulWidget {
142
142
  final SharedPreferences sharedPreferences;
143
143
 
144
144
  const MyApp({
@@ -146,23 +146,42 @@ class MyApp extends ConsumerWidget {
146
146
  required this.sharedPreferences,
147
147
  });
148
148
 
149
+ @override
150
+ ConsumerState<MyApp> createState() => _MyAppState();
151
+ }
152
+
153
+ class _MyAppState extends ConsumerState<MyApp> {
154
+ late final AppTheme _appTheme;
155
+
156
+ @override
157
+ void initState() {
158
+ super.initState();
159
+ _appTheme = AppTheme.uniform(
160
+ sharedPreferences: widget.sharedPreferences,
161
+ themeFactory: const UniversalThemeFactory(),
162
+ lightColors: KasyColors.light(),
163
+ darkColors: KasyColors.dark(),
164
+ textTheme: KasyTextTheme.build(),
165
+ defaultMode: ThemeMode.system,
166
+ );
167
+ }
168
+
169
+ @override
170
+ void dispose() {
171
+ _appTheme.dispose();
172
+ super.dispose();
173
+ }
174
+
149
175
  // This widget is the root of your application.
150
176
  @override
151
- Widget build(BuildContext context, WidgetRef ref) {
177
+ Widget build(BuildContext context) {
152
178
  ErrorWidget.builder = (FlutterErrorDetails details) {
153
179
  return AppErrorWidget(error: details);
154
180
  };
155
181
  final goRouter = ref.watch(goRouterProvider);
156
182
 
157
183
  return ThemeProvider(
158
- notifier: AppTheme.uniform(
159
- sharedPreferences: sharedPreferences,
160
- themeFactory: const UniversalThemeFactory(),
161
- lightColors: KasyColors.light(),
162
- darkColors: KasyColors.dark(),
163
- textTheme: KasyTextTheme.build(),
164
- defaultMode: ThemeMode.light,
165
- ),
184
+ notifier: _appTheme,
166
185
  child: Builder(builder: (context) {
167
186
  return WebDevicePreview.wrap(
168
187
  child: DevInspector.wrap(
@@ -1,151 +1,27 @@
1
1
  # Feature Patches
2
2
 
3
- Este diretório contém os **patches por feature**: arquivos extras que são copiados para o projeto gerado quando o usuário seleciona um módulo específico no wizard do CLI.
3
+ Este diretório guarda **patches por feature**: arquivos extras que são copiados para o projeto gerado quando o usuário seleciona um módulo específico no wizard do CLI.
4
4
 
5
- ## Estrutura esperada
5
+ ## Estado atual
6
6
 
7
- ```
8
- features/
9
- ci/ ← patch aplicado quando o usuário seleciona o módulo "ci"
10
- .github/
11
- workflows/
12
- app.yml
13
- .gitlab-ci.yml
14
- ...
15
- web/ ← patch aplicado quando "web" é selecionado
16
- web/
17
- index.html
18
- ...
19
- widget/ ← patch aplicado quando "widget" é selecionado
20
- android/
21
- ...
22
- ```
7
+ Este diretório está intencionalmente vazio (só este README). Todas as features já vivem dentro de `Firebase/` e são incluídas em `cli/templates/firebase/` pelo `cli/scripts/bundle-template.js` no momento do publish. Features opcionais são controladas por `removeModuleDirs()` em `cli/lib/scaffold/shared/generator-utils.js` — quando a feature não é selecionada, sua pasta é removida do projeto gerado.
23
8
 
24
- Cada subdiretório tem o **mesmo nome** que o valor do módulo em `new.js` (`sentry`, `analytics`, `facebook`, `revenuecat`, `onboarding`, `web`, `widget`, `llm_chat`, `local_notifications`, `feedback`, `ci`).
9
+ ## Quando criar um patch aqui
25
10
 
26
- Ao gerar um projeto, o engine copia recursivamente o conteúdo de `features/{modulo}/` para a raiz do projeto, sobrescrevendo ou acrescentando arquivos.
11
+ Apenas quando a feature precisar de arquivos que **não existem** em `Firebase/`. Se o mesmo caminho já existe em `Firebase/`, **não crie patch** — o patch vai sobrescrever silenciosamente o template atualizado pela versão (provavelmente antiga) que estiver no patch, causando regressão no projeto do cliente (perda de integrações, validações, UI nova, etc.).
27
12
 
28
- ## Módulos que precisam de patch físico
13
+ ## Como adicionar um patch
29
14
 
30
- | Módulo | Patch necessário? | Motivo |
31
- |-------------|------------------|-----------------------------------------------------|
32
- | `ci` | Sim | Adiciona `.github/`, `.gitlab-ci.yml`, etc. |
33
- | `web` | Sim | Adiciona pasta `web/` e configurações de plataforma |
34
- | `widget` | Não | Os arquivos do widget (iOS + Android + Dart) já vão no template base. Quando o módulo não é selecionado, `removeAndroidWidgetArtifacts` + `writeNoOpAdminHomeWidgets` em `generate.js` limpam o que sobra. |
35
- | `llm_chat` | Não | Apenas `LLM_CHAT_ENDPOINT` via dart-define. A chave da API LLM fica no servidor (Firebase Secret / Supabase Secret) — nunca no app. |
36
- | `sentry` | Não | Apenas dart-define (`SENTRY_DSN`) |
37
- | `analytics` | Não | Apenas dart-define |
38
- | `facebook` | Não | Token já substituído via `buildTokens` |
39
- | `revenuecat`| Não | Apenas dart-define (`REVENUECAT_KEY_*`) |
40
- | `onboarding`| Não | Habilitado via dart-define / `features_config.json` |
41
- | `feedback` | Não | Habilitado via dart-define / `features_config.json` |
42
- | `local_notifications` | ✅ Sim | Copia `lib/features/local_reminder/` com UI, provider e repositório |
15
+ 1. Crie `features/{modulo}/`.
16
+ 2. Coloque os arquivos exatamente como devem aparecer no projeto gerado (caminhos relativos à raiz do projeto).
17
+ 3. Adicione o nome do módulo a `ALLOWED_FEATURE_PATCHES` em `cli/scripts/check-feature-patches.js`, com um comentário de uma linha explicando por que o patch é necessário.
18
+ 4. O engine aplica os patches depois de copiar o template base e o patch do backend.
43
19
 
44
- ## Como criar um patch
20
+ ## Drift guard
45
21
 
46
- 1. Crie a pasta `features/{modulo}/`.
47
- 2. Coloque dentro dela os arquivos **exatamente como devem aparecer** no projeto gerado (caminhos relativos à raiz do projeto).
48
- 3. O engine aplica os patches após copiar o template base e após aplicar o patch do backend.
22
+ `cli/scripts/check-feature-patches.js` roda no `npm prepack` (antes de qualquer `npm publish` ou `npm pack`). O build falha se:
49
23
 
50
- > Os patches de feature são aplicados pelo `generate.js` via `applyFeaturePatches()`.
51
- > Os geradores específicos por backend (`firebase/generator.js`, `supabase/generator.js`, `api/generator.js`)
52
- > precisam ser atualizados para chamar essa etapa quando a unificação for concluída.
24
+ - alguma pasta dentro de `features/` não estiver listada em `ALLOWED_FEATURE_PATCHES`, ou
25
+ - algum arquivo dentro de um patch permitido tiver caminho equivalente em `Firebase/` (significa que o patch é duplicata desatualizada).
53
26
 
54
- ---
55
-
56
- ## Feature: `local_notifications` — Notificações Locais Agendadas
57
-
58
- > **Plataformas:** iOS e Android apenas (sem suporte Web/Desktop).
59
-
60
- ### O que esta feature faz
61
-
62
- Permite ao usuário configurar um lembrete recorrente diretamente no app, sem servidor. A notificação é agendada localmente via `flutter_local_notifications` e persiste entre sessões via `SharedPreferences`.
63
-
64
- ### Tipos de lembrete suportados
65
-
66
- | Tipo | Comportamento |
67
- |-----------------|------------------------------------------------------|
68
- | `daily` | Dispara todo dia no horário escolhido |
69
- | `weekly` | Dispara uma vez por semana no dia + horário escolhido |
70
- | `specificDate` | Dispara uma única vez na data + hora escolhida |
71
-
72
- ### Entrada do usuário
73
-
74
- Acesso via **Settings → Lembretes** (`/reminder`):
75
-
76
- 1. **Toggle "Ativar lembrete"** — liga/desliga (cancela notificação pendente ao desligar)
77
- 2. **Segmented button "Repetir"** — escolhe o tipo: `Todo dia / Toda semana / Data específica`
78
- 3. **Horário** — `TimePicker` nativo do sistema
79
- 4. **Dia da semana** — `ChoiceChip` (seg–dom), aparece somente no modo semanal
80
- 5. **Data e hora** — `DatePicker` + `TimePicker` em sequência, aparece somente no modo data específica
81
-
82
- ### Estrutura de arquivos gerados no projeto
83
-
84
- ```
85
- lib/features/local_reminder/
86
- repositories/
87
- reminder_preferences.dart ← SharedPreferences (load/save ReminderState)
88
- providers/
89
- reminder_notifier.dart ← AsyncNotifier (keepAlive) + agenda/cancela
90
- reminder_notifier.g.dart ← gerado pelo build_runner (provider: reminderProvider)
91
- ui/
92
- reminder_page.dart ← ReminderPage + _ReminderForm + subwidgets
93
- ```
94
-
95
- ### Integrações automáticas pelo CLI
96
-
97
- | Arquivo | O que é adicionado |
98
- |--------------------------------|---------------------------------------------------------|
99
- | `lib/core/config/features.dart` | `const bool withLocalNotifications = true/false;` |
100
- | `lib/router.dart` | Import + `GoRoute(path: '/reminder', ...)` |
101
- | `lib/features/settings/settings_page.dart` | Tile "Lembretes" → `context.push('/reminder')` |
102
-
103
- ### Model — `ReminderState`
104
-
105
- ```dart
106
- class ReminderState {
107
- final bool enabled;
108
- final ReminderType type; // daily | weekly | specificDate
109
- final int hour;
110
- final int minute;
111
- final int dayOfWeek; // 1=Segunda … 7=Domingo
112
- final DateTime? date; // usado somente para specificDate
113
- }
114
- ```
115
-
116
- ### Notificação agendada
117
-
118
- ID fixo `42`. Texto configurado via i18n (`dailyReminder.title` / `dailyReminder.body`).
119
- Ao salvar qualquer mudança, o provider cancela o ID 42 e re-agenda com os novos parâmetros.
120
-
121
- ### Dependências (já no core — não adicionadas pelo módulo)
122
-
123
- - `flutter_local_notifications`
124
- - `flutter_timezone` + `timezone`
125
- - `shared_preferences`
126
- - `riverpod_annotation`
127
-
128
- ### Traduções adicionadas
129
-
130
- Chaves em `lib/i18n/{pt,en,es}.i18n.json`:
131
-
132
- ```json
133
- "reminderPage": {
134
- "title": "Lembretes",
135
- "toggleLabel": "Ativar lembrete",
136
- "typeLabel": "Repetir",
137
- "daily": "Todo dia",
138
- "weekly": "Toda semana",
139
- "specificDate": "Data específica",
140
- "timeLabel": "Horário",
141
- "dayLabel": "Dia da semana",
142
- "dateLabel": "Data e hora",
143
- "selectDate": "Selecionar data e hora"
144
- },
145
- "dailyReminder": { "title": "Lembrete", "body": "Está na hora de beber água." },
146
- "settings": { "reminders": "Lembretes" }
147
- ```
148
-
149
- ### Bug corrigido durante a implementação
150
-
151
- `local_notifier.dart` tinha um método `scheduleFromNow()` com lógica invertida (agendava apenas quando **não** havia notificações pendentes) e usava `Future.delayed` que não sobrevive ao restart do app. O método foi **removido** completamente.
27
+ Esse é o mecanismo que impede o cenário em que patches divergem de `Firebase/` ao longo do tempo e o CLI passa a entregar código velho para os clientes.
@@ -273,7 +273,6 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
273
273
  lines.push(`import 'package:${pkg}/features/authentication/ui/signin_page.dart';`);
274
274
  lines.push(`import 'package:${pkg}/features/authentication/ui/signup_page.dart';`);
275
275
  if (withFeedback) {
276
- lines.push(`import 'package:${pkg}/features/feedbacks/ui/component/add_feature_form.dart';`);
277
276
  lines.push(`import 'package:${pkg}/features/feedbacks/ui/feedback_page.dart';`);
278
277
  }
279
278
  if (withLlmChat) {
@@ -374,11 +373,6 @@ async function writeRouter(projectDir, modules, packageName, defaultPaywall = 'b
374
373
  lines.push(` path: '/feedback',`);
375
374
  lines.push(` builder: (context, state) => const FeedbackPage(),`);
376
375
  lines.push(` ),`);
377
- lines.push(` GoRoute(`);
378
- lines.push(` name: 'feedback_new',`);
379
- lines.push(` path: '/feedback/new',`);
380
- lines.push(` builder: (context, state) => const AddFeatureComponent(),`);
381
- lines.push(` ),`);
382
376
  }
383
377
 
384
378
  // LLM Chat
@@ -596,6 +590,13 @@ class NoOpAnalyticsApi implements AnalyticsApi {
596
590
  @override Future<void> identify(User user) async {}
597
591
  }
598
592
 
593
+ // Stub kept for source-compat with code that still calls MixpanelAnalyticsApi.instance().
594
+ // Run: kasy add analytics to swap this file for the real Mixpanel-backed implementation.
595
+ class MixpanelAnalyticsApi extends NoOpAnalyticsApi {
596
+ const MixpanelAnalyticsApi._() : super();
597
+ factory MixpanelAnalyticsApi.instance() => const MixpanelAnalyticsApi._();
598
+ }
599
+
599
600
  class AnalyticsObserver extends RouteObserver<ModalRoute<dynamic>> {
600
601
  final AnalyticsApi analyticsApi;
601
602
  final String? prefix;
@@ -1186,11 +1187,8 @@ async function stripPubspecDeps(projectDir, modules) {
1186
1187
  }
1187
1188
 
1188
1189
  /**
1189
- * Patches core files that always import sentry_flutter, removing the dependency
1190
+ * Patches files that always import sentry_flutter, removing the dependency
1190
1191
  * when neither sentry, revenuecat, nor facebook modules are selected.
1191
- * Affected files:
1192
- * - lib/core/initializer/onstart_widget.dart
1193
- * - lib/core/data/api/remote_config_api.dart
1194
1192
  *
1195
1193
  * @param {string} projectDir
1196
1194
  */
@@ -1200,15 +1198,16 @@ async function writeNoOpSentryUsages(projectDir) {
1200
1198
  const files = [
1201
1199
  path.join(projectDir, 'lib', 'core', 'initializer', 'onstart_widget.dart'),
1202
1200
  path.join(projectDir, 'lib', 'core', 'data', 'api', 'remote_config_api.dart'),
1201
+ path.join(projectDir, 'lib', 'features', 'notifications', 'api', 'local_notifier.dart'),
1202
+ path.join(projectDir, 'lib', 'features', 'notifications', 'providers', 'models', 'notification.dart'),
1203
+ path.join(projectDir, 'lib', 'features', 'notifications', 'shared', 'notification_permission_bottom_sheet.dart'),
1203
1204
  ];
1204
1205
 
1205
1206
  for (const filePath of files) {
1206
1207
  if (!(await fs.pathExists(filePath))) continue;
1207
1208
  let content = await fs.readFile(filePath, 'utf8');
1208
- // Remove sentry import line
1209
1209
  content = content.replace(sentryImport, '');
1210
- // Remove single-line Sentry.captureException(...); calls (with leading whitespace)
1211
- content = content.replace(/^[ \t]*Sentry\.captureException\([^)]+\);\n/gm, '');
1210
+ content = content.replace(/^[ \t]*Sentry\.captureException\([^)]*\);\n/gm, '');
1212
1211
  await fs.outputFile(filePath, content, 'utf8');
1213
1212
  }
1214
1213
  }
@@ -1469,11 +1468,13 @@ class PremiumPageArgs {
1469
1468
  'utf8',
1470
1469
  );
1471
1470
 
1472
- // 4. No-op premium_page_factory.dart (used by admin_paywalls)
1471
+ // 4. No-op premium_page_factory.dart (used by admin_paywalls).
1472
+ // Mirror the constants declared in Firebase/.../premium_page_factory.dart so that
1473
+ // admin_routes.dart compiles even without revenuecat selected.
1473
1474
  await fs.outputFile(
1474
1475
  path.join(projectDir, 'lib', 'features', 'subscription', 'ui', 'component', 'premium_page_factory.dart'),
1475
1476
  `// No-op paywall factory. Run: kasy add revenuecat to activate.
1476
- enum PaywallFactory { basic }
1477
+ enum PaywallFactory { basic, basicRow, minimal, withSwitch }
1477
1478
  `,
1478
1479
  'utf8',
1479
1480
  );