kasy-cli 1.13.0 → 1.15.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 (157) hide show
  1. package/bin/kasy.js +140 -12
  2. package/lib/commands/add.js +2 -2
  3. package/lib/commands/codemagic.js +11 -4
  4. package/lib/commands/deploy.js +3 -3
  5. package/lib/commands/favicon.js +115 -0
  6. package/lib/commands/icon.js +143 -0
  7. package/lib/commands/ios.js +28 -7
  8. package/lib/commands/new.js +8 -20
  9. package/lib/commands/remove.js +1 -1
  10. package/lib/commands/reset.js +385 -0
  11. package/lib/commands/run.js +24 -17
  12. package/lib/commands/splash.js +14 -4
  13. package/lib/commands/update.js +1 -1
  14. package/lib/scaffold/backends/api/patch/README.md +1 -1
  15. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
  17. package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
  18. package/lib/scaffold/backends/firebase/tokens.js +2 -2
  19. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
  20. package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
  21. package/lib/scaffold/backends/supabase/patch/README.md +1 -1
  22. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
  23. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
  24. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
  25. package/lib/utils/apple-release.js +115 -16
  26. package/lib/utils/checks.js +45 -107
  27. package/lib/utils/debug.js +75 -0
  28. package/lib/utils/flutter-run.js +173 -0
  29. package/lib/utils/friendly-error.js +91 -0
  30. package/lib/utils/i18n/messages-en.js +970 -0
  31. package/lib/utils/i18n/messages-es.js +968 -0
  32. package/lib/utils/i18n/messages-pt.js +968 -0
  33. package/lib/utils/i18n.js +21 -2483
  34. package/lib/utils/mobile-identity.js +35 -0
  35. package/lib/utils/png-padding.js +120 -0
  36. package/lib/utils/ui.js +114 -0
  37. package/package.json +8 -4
  38. package/templates/firebase/README.en.md +1 -1
  39. package/templates/firebase/README.es.md +1 -1
  40. package/templates/firebase/README.md +1 -1
  41. package/templates/firebase/android/app/build.gradle.kts +10 -1
  42. package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
  43. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
  44. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +161 -11
  45. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
  46. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
  47. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
  48. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
  49. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
  50. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  62. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  63. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  64. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  65. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  66. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  67. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  68. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  69. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  70. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  71. package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
  72. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +53 -0
  73. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  74. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
  80. package/templates/firebase/assets/images/favicon.png +0 -0
  81. package/templates/firebase/assets/images/icon.png +0 -0
  82. package/templates/firebase/assets/images/icon_android.png +0 -0
  83. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  84. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  85. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  86. package/templates/firebase/firestore.indexes.json +10 -0
  87. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
  88. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
  89. package/templates/firebase/functions/src/index.ts +1 -0
  90. package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
  91. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
  92. package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
  93. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  94. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  96. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  98. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  99. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  100. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  101. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  102. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  103. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  104. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  105. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  106. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  107. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  108. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  109. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  110. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  111. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  112. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  113. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  114. package/templates/firebase/ios/Runner/Info.plist +2 -2
  115. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
  116. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
  117. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
  118. package/templates/firebase/lib/components/components.dart +1 -0
  119. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  120. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  121. package/templates/firebase/lib/components/kasy_button.dart +8 -0
  122. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  123. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +73 -19
  125. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
  126. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
  127. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
  129. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  130. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  131. package/templates/firebase/lib/features/home/home_page.dart +0 -6
  132. package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
  133. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
  134. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
  135. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
  136. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
  137. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
  138. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
  139. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
  140. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
  141. package/templates/firebase/lib/i18n/en.i18n.json +4 -1
  142. package/templates/firebase/lib/i18n/es.i18n.json +4 -1
  143. package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
  144. package/templates/firebase/pubspec.yaml +10 -3
  145. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
  146. package/templates/firebase/web/favicon.png +0 -0
  147. package/templates/firebase/web/icons/Icon-192.png +0 -0
  148. package/templates/firebase/web/icons/Icon-512.png +0 -0
  149. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  150. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  151. package/templates/firebase/web/index.html +9 -0
  152. package/templates/firebase/web/manifest.json +3 -3
  153. package/templates/firebase/assets/images/app_icon.png +0 -0
  154. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  155. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  156. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
  157. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
@@ -0,0 +1,35 @@
1
+ const path = require('node:path');
2
+ const fs = require('fs-extra');
3
+ const { readBundleId } = require('./apple-release');
4
+
5
+ /**
6
+ * Reads the Android applicationId (package name) of the project.
7
+ * Source of truth is build.gradle(.kts) — what gets baked into the APK and
8
+ * what the device sees. kit_setup.json can drift when the user renames the
9
+ * package manually after `kasy new`, so it's only used as a fallback when
10
+ * the Android project hasn't been generated yet.
11
+ */
12
+ async function readPackageName(projectDir) {
13
+ const candidates = [
14
+ path.join(projectDir, 'android', 'app', 'build.gradle.kts'),
15
+ path.join(projectDir, 'android', 'app', 'build.gradle'),
16
+ ];
17
+ for (const file of candidates) {
18
+ if (!(await fs.pathExists(file))) continue;
19
+ const content = await fs.readFile(file, 'utf8');
20
+ const m = content.match(/applicationId\s*=?\s*["']([^"']+)["']/);
21
+ if (m) return m[1];
22
+ }
23
+ const kitSetupPath = path.join(projectDir, 'kit_setup.json');
24
+ if (await fs.pathExists(kitSetupPath)) {
25
+ try {
26
+ const kit = await fs.readJson(kitSetupPath);
27
+ if (kit.bundleId) return kit.bundleId;
28
+ } catch {
29
+ // ignore
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ module.exports = { readBundleId, readPackageName };
@@ -0,0 +1,120 @@
1
+ const fsp = require('node:fs/promises');
2
+ const { PNG } = require('pngjs');
3
+
4
+ const ANDROID12_SAFE_RATIO = 0.5;
5
+
6
+ async function readPng(filePath) {
7
+ const buffer = await fsp.readFile(filePath);
8
+ return new Promise((resolve, reject) => {
9
+ new PNG().parse(buffer, (err, data) => {
10
+ if (err) reject(err);
11
+ else resolve(data);
12
+ });
13
+ });
14
+ }
15
+
16
+ function resizeBilinear(src, dstW, dstH) {
17
+ const dst = new PNG({ width: dstW, height: dstH });
18
+ const srcW = src.width;
19
+ const srcH = src.height;
20
+ const xRatio = srcW > 1 ? (srcW - 1) / dstW : 0;
21
+ const yRatio = srcH > 1 ? (srcH - 1) / dstH : 0;
22
+
23
+ for (let y = 0; y < dstH; y++) {
24
+ const sy = (y + 0.5) * yRatio;
25
+ const y0 = Math.floor(sy);
26
+ const y1 = Math.min(y0 + 1, srcH - 1);
27
+ const wy = sy - y0;
28
+
29
+ for (let x = 0; x < dstW; x++) {
30
+ const sx = (x + 0.5) * xRatio;
31
+ const x0 = Math.floor(sx);
32
+ const x1 = Math.min(x0 + 1, srcW - 1);
33
+ const wx = sx - x0;
34
+
35
+ const i00 = (y0 * srcW + x0) * 4;
36
+ const i10 = (y0 * srcW + x1) * 4;
37
+ const i01 = (y1 * srcW + x0) * 4;
38
+ const i11 = (y1 * srcW + x1) * 4;
39
+
40
+ const dstIdx = (y * dstW + x) * 4;
41
+ for (let c = 0; c < 4; c++) {
42
+ const top = src.data[i00 + c] * (1 - wx) + src.data[i10 + c] * wx;
43
+ const bot = src.data[i01 + c] * (1 - wx) + src.data[i11 + c] * wx;
44
+ dst.data[dstIdx + c] = Math.round(top * (1 - wy) + bot * wy);
45
+ }
46
+ }
47
+ }
48
+ return dst;
49
+ }
50
+
51
+ function compositeOnTransparentSquare(logo, canvasSize) {
52
+ const canvas = new PNG({ width: canvasSize, height: canvasSize });
53
+ canvas.data.fill(0);
54
+
55
+ const offsetX = Math.floor((canvasSize - logo.width) / 2);
56
+ const offsetY = Math.floor((canvasSize - logo.height) / 2);
57
+
58
+ for (let y = 0; y < logo.height; y++) {
59
+ for (let x = 0; x < logo.width; x++) {
60
+ const srcIdx = (y * logo.width + x) * 4;
61
+ const dstIdx = ((y + offsetY) * canvasSize + (x + offsetX)) * 4;
62
+ canvas.data[dstIdx] = logo.data[srcIdx];
63
+ canvas.data[dstIdx + 1] = logo.data[srcIdx + 1];
64
+ canvas.data[dstIdx + 2] = logo.data[srcIdx + 2];
65
+ canvas.data[dstIdx + 3] = logo.data[srcIdx + 3];
66
+ }
67
+ }
68
+ return canvas;
69
+ }
70
+
71
+ async function writePng(png, filePath) {
72
+ return new Promise((resolve, reject) => {
73
+ const chunks = [];
74
+ png.pack()
75
+ .on('data', (chunk) => chunks.push(chunk))
76
+ .on('end', async () => {
77
+ try {
78
+ await fsp.writeFile(filePath, Buffer.concat(chunks));
79
+ resolve();
80
+ } catch (e) {
81
+ reject(e);
82
+ }
83
+ })
84
+ .on('error', reject);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Read a PNG and write a new one of the same dimensions, with the source
90
+ * logo scaled down to fit inside the Android 12+ splash safe area (centered,
91
+ * transparent padding around it). This is what `windowSplashScreenAnimatedIcon`
92
+ * needs so the OS-applied circular mask doesn't clip the logo edges.
93
+ *
94
+ * @param {string} srcPath
95
+ * @param {string} dstPath
96
+ * @param {number} safeRatio fraction of the canvas the logo should occupy (default 0.6)
97
+ */
98
+ async function writeAndroid12Variant(srcPath, dstPath, safeRatio = ANDROID12_SAFE_RATIO) {
99
+ const src = await readPng(srcPath);
100
+ const canvasSize = Math.max(src.width, src.height);
101
+ const safeSide = Math.round(canvasSize * safeRatio);
102
+
103
+ const aspect = src.width / src.height;
104
+ let logoW;
105
+ let logoH;
106
+ if (aspect >= 1) {
107
+ logoW = safeSide;
108
+ logoH = Math.round(safeSide / aspect);
109
+ } else {
110
+ logoH = safeSide;
111
+ logoW = Math.round(safeSide * aspect);
112
+ }
113
+
114
+ const resized = resizeBilinear(src, logoW, logoH);
115
+ const composited = compositeOnTransparentSquare(resized, canvasSize);
116
+ await writePng(composited, dstPath);
117
+ return { canvasSize, logoW, logoH };
118
+ }
119
+
120
+ module.exports = { writeAndroid12Variant, ANDROID12_SAFE_RATIO };
package/lib/utils/ui.js CHANGED
@@ -13,6 +13,15 @@
13
13
  */
14
14
 
15
15
  const clack = require('@clack/prompts');
16
+ const kleur = require('kleur');
17
+
18
+ function formatElapsedSeconds(startTime) {
19
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
20
+ if (elapsed < 60) return `${elapsed}s`;
21
+ const m = Math.floor(elapsed / 60);
22
+ const s = elapsed % 60;
23
+ return `${m}m ${s}s`;
24
+ }
16
25
 
17
26
  function handleCancel(result, onCancel) {
18
27
  if (clack.isCancel(result)) {
@@ -85,6 +94,109 @@ function cancel(message) { clack.cancel(message); }
85
94
  */
86
95
  function spinner() { return clack.spinner(); }
87
96
 
97
+ /**
98
+ * Spinner with an automatic elapsed-time suffix that ticks every second.
99
+ * Same API as spinner() — useful for operations that take >30s so the user
100
+ * sees the clock moving even when the underlying tool emits no progress.
101
+ *
102
+ * const s = ui.timedSpinner();
103
+ * s.start('Deploying…'); // "Deploying… [0s]"
104
+ * // ... 73 seconds later
105
+ * s.stop('Deploy done'); // "Deploy done [1m 13s]"
106
+ */
107
+ function timedSpinner() {
108
+ const s = clack.spinner();
109
+ let startTime = null;
110
+ let currentMessage = '';
111
+ let tick = null;
112
+
113
+ const render = (msg) => {
114
+ if (!msg) return '';
115
+ if (!startTime) return msg;
116
+ return `${msg} ${kleur.dim(`[${formatElapsedSeconds(startTime)}]`)}`;
117
+ };
118
+
119
+ const stopTick = () => {
120
+ if (tick) { clearInterval(tick); tick = null; }
121
+ };
122
+
123
+ return {
124
+ start(msg) {
125
+ startTime = Date.now();
126
+ currentMessage = msg || '';
127
+ s.start(render(currentMessage));
128
+ tick = setInterval(() => {
129
+ if (currentMessage) s.message(render(currentMessage));
130
+ }, 1000);
131
+ },
132
+ message(msg) {
133
+ currentMessage = msg || '';
134
+ s.message(render(currentMessage));
135
+ },
136
+ stop(msg, code) {
137
+ stopTick();
138
+ const finalMsg = msg != null ? render(msg) : render(currentMessage);
139
+ s.stop(finalMsg, code);
140
+ },
141
+ error(msg) {
142
+ stopTick();
143
+ s.error(msg != null ? render(msg) : render(currentMessage));
144
+ },
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Multi-step spinner where every step shows its own elapsed-time counter.
150
+ * Use for long sequential flows like Firebase project creation so the user
151
+ * can tell which steps are slow.
152
+ */
153
+ function makeTimedStepper() {
154
+ let current = null;
155
+ let currentMsg = '';
156
+ return {
157
+ next(text) {
158
+ if (current) current.stop(currentMsg);
159
+ current = timedSpinner();
160
+ currentMsg = text;
161
+ current.start(text);
162
+ },
163
+ update(text) {
164
+ if (current) {
165
+ currentMsg = text;
166
+ current.message(text);
167
+ }
168
+ },
169
+ succeed(text) {
170
+ if (current) {
171
+ current.stop(text || currentMsg);
172
+ current = null;
173
+ currentMsg = '';
174
+ }
175
+ },
176
+ fail(text) {
177
+ if (current) {
178
+ current.error(text || currentMsg);
179
+ current = null;
180
+ currentMsg = '';
181
+ }
182
+ },
183
+ warn(text) {
184
+ if (current) {
185
+ current.stop(`⚠ ${text || currentMsg}`);
186
+ current = null;
187
+ currentMsg = '';
188
+ }
189
+ },
190
+ stop() {
191
+ if (current) {
192
+ current.stop(currentMsg);
193
+ current = null;
194
+ currentMsg = '';
195
+ }
196
+ },
197
+ };
198
+ }
199
+
88
200
  /**
89
201
  * Multi-step spinner: each .next(text) succeeds the previous step
90
202
  * with the previous message, then starts a new step with `text`.
@@ -166,7 +278,9 @@ module.exports = {
166
278
  note,
167
279
  cancel,
168
280
  spinner,
281
+ timedSpinner,
169
282
  makeStepper,
283
+ makeTimedStepper,
170
284
  taskLog,
171
285
  progress,
172
286
  log,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -39,10 +39,12 @@
39
39
  "validate": "node ./bin/kasy.js validate --analyze-only",
40
40
  "extract:patch": "node ./scripts/extract_patch.js",
41
41
  "check:firebase": "node ./scripts/check-firebase-template.js",
42
+ "test": "for f in test/*.test.js; do node \"$f\" || exit 1; done",
42
43
  "test:google-ios": "node ./test/google-ios-url-scheme.test.js",
43
44
  "test:apple-release": "node ./test/apple-release.test.js",
44
45
  "test:localize-docs": "node ./test/localize-release-docs.test.js",
45
- "test:i18n-accents": "node ./test/i18n-accents.test.js"
46
+ "test:i18n-accents": "node ./test/i18n-accents.test.js",
47
+ "lint": "eslint bin lib scripts"
46
48
  },
47
49
  "dependencies": {
48
50
  "@clack/prompts": "^1.4.0",
@@ -51,8 +53,10 @@
51
53
  "fs-extra": "^11.2.0",
52
54
  "gradient-string": "^1.2.0",
53
55
  "kleur": "^4.1.5",
54
- "ora": "^8.0.1",
55
- "prompts": "^2.4.2",
56
+ "pngjs": "^7.0.0",
56
57
  "yaml": "^2.4.2"
58
+ },
59
+ "devDependencies": {
60
+ "eslint": "^9.39.4"
57
61
  }
58
62
  }
@@ -1,4 +1,4 @@
1
- # AppFirebase
1
+ # Kasy App
2
2
 
3
3
  Flutter app with Firebase backend — generated by kasy.
4
4
 
@@ -1,4 +1,4 @@
1
- # AppFirebase
1
+ # Kasy App
2
2
 
3
3
  App Flutter con backend Firebase — generado por kasy.
4
4
 
@@ -1,4 +1,4 @@
1
- # AppFirebase
1
+ # Kasy App
2
2
 
3
3
  Flutter app com backend Firebase — gerado pelo kasy.
4
4
 
@@ -76,5 +76,14 @@ flutter {
76
76
  dependencies {
77
77
  coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
78
78
  implementation("com.google.android.gms:play-services-ads-identifier:18.1.0")
79
- implementation("androidx.glance:glance-appwidget:1.1.1")
79
+ // strictly() blocks Gradle from upgrading via transitive resolution —
80
+ // the home_widget plugin declares `1.+` and would otherwise pick the
81
+ // latest alpha (1.3.0-alpha01 at time of writing), which requires
82
+ // compileSdk 37 + AGP 9.x and breaks the build.
83
+ implementation("androidx.glance:glance-appwidget") {
84
+ version {
85
+ strictly("1.1.1")
86
+ }
87
+ }
88
+ implementation("androidx.appcompat:appcompat:1.7.0")
80
89
  }
@@ -16,7 +16,7 @@
16
16
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
17
17
 
18
18
  <application
19
- android:label="AppFirebase"
19
+ android:label="Kasy App"
20
20
  android:name="${applicationName}"
21
21
  android:icon="@mipmap/ic_launcher"
22
22
  android:localeConfig="@xml/locales_config">
@@ -1,5 +1,8 @@
1
1
  package com.aicrus.firebase.kit
2
-
2
+
3
+ import android.content.Context
4
+ import android.os.Bundle
5
+ import androidx.appcompat.app.AppCompatDelegate
3
6
  import com.google.android.gms.ads.identifier.AdvertisingIdClient
4
7
  import io.flutter.embedding.android.FlutterActivity
5
8
  import io.flutter.embedding.engine.FlutterEngine
@@ -12,6 +15,27 @@ import kotlinx.coroutines.withContext
12
15
  class MainActivity : FlutterActivity() {
13
16
  private val CHANNEL = "kasy_kit/advertising_id"
14
17
 
18
+ override fun onCreate(savedInstanceState: Bundle?) {
19
+ applySavedThemeMode()
20
+ super.onCreate(savedInstanceState)
21
+ }
22
+
23
+ // Forces the night mode to match the user's saved theme preference (read
24
+ // from `shared_preferences`) so the native splash drawable selection
25
+ // (drawable-night vs drawable) follows the in-app choice, not just the OS.
26
+ private fun applySavedThemeMode() {
27
+ val prefs = applicationContext.getSharedPreferences(
28
+ "FlutterSharedPreferences",
29
+ Context.MODE_PRIVATE,
30
+ )
31
+ val mode = when (prefs.getString("flutter.themeMode", null)) {
32
+ "dark" -> AppCompatDelegate.MODE_NIGHT_YES
33
+ "light" -> AppCompatDelegate.MODE_NIGHT_NO
34
+ else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
35
+ }
36
+ AppCompatDelegate.setDefaultNightMode(mode)
37
+ }
38
+
15
39
  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
16
40
  super.configureFlutterEngine(flutterEngine)
17
41
 
@@ -1,32 +1,50 @@
1
1
  package com.aicrus.firebase.kit
2
2
 
3
3
  import android.content.Context
4
+ import android.content.Intent
4
5
  import androidx.compose.runtime.Composable
5
6
  import androidx.compose.ui.graphics.Color
6
7
  import androidx.compose.ui.unit.dp
7
8
  import androidx.compose.ui.unit.sp
8
9
  import androidx.glance.GlanceId
9
10
  import androidx.glance.GlanceModifier
11
+ import androidx.glance.Image
12
+ import androidx.glance.ImageProvider
13
+ import androidx.glance.LocalSize
14
+ import androidx.glance.action.clickable
10
15
  import androidx.glance.appwidget.GlanceAppWidget
16
+ import androidx.glance.appwidget.SizeMode
17
+ import androidx.glance.appwidget.action.actionStartActivity
11
18
  import androidx.glance.appwidget.provideContent
12
19
  import androidx.glance.background
13
20
  import androidx.glance.currentState
14
21
  import androidx.glance.layout.Alignment
15
22
  import androidx.glance.layout.Box
16
23
  import androidx.glance.layout.Column
24
+ import androidx.glance.layout.ContentScale
25
+ import androidx.glance.layout.Row
17
26
  import androidx.glance.layout.Spacer
18
27
  import androidx.glance.layout.fillMaxSize
28
+ import androidx.glance.layout.fillMaxWidth
19
29
  import androidx.glance.layout.padding
30
+ import androidx.glance.layout.size
20
31
  import androidx.glance.state.GlanceStateDefinition
32
+ import androidx.glance.text.FontStyle
21
33
  import androidx.glance.text.FontWeight
22
34
  import androidx.glance.text.Text
23
35
  import androidx.glance.text.TextStyle
24
36
  import androidx.glance.unit.ColorProvider
25
37
  import es.antonborri.home_widget.HomeWidgetGlanceState
26
38
  import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
39
+ import java.util.Calendar
40
+ import java.util.Locale
27
41
 
28
42
  class MyWidgetWidget : GlanceAppWidget() {
29
43
 
44
+ // Recompose when the user resizes the widget so the layout can adapt
45
+ // between small (no "+", no quote) and large (everything visible).
46
+ override val sizeMode: SizeMode = SizeMode.Exact
47
+
30
48
  override val stateDefinition: GlanceStateDefinition<*>?
31
49
  get() = HomeWidgetGlanceStateDefinition()
32
50
 
@@ -39,22 +57,49 @@ class MyWidgetWidget : GlanceAppWidget() {
39
57
  @Composable
40
58
  private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
41
59
  val prefs = currentState.preferences
42
- val greeting = prefs.getString("greeting", "") ?: ""
43
- val title = prefs.getString("title", "") ?: ""
60
+ val storedGreeting = prefs.getString("greeting", "") ?: ""
61
+ val storedTitle = prefs.getString("title", "") ?: ""
44
62
  val planText = prefs.getString("planText", "") ?: ""
45
63
  val isPro = prefs.getString("isPro", "false") == "true"
64
+ val quote = prefs.getString("quote", "") ?: ""
65
+
66
+ // Time/locale-based fallback used when Flutter has not pushed data yet —
67
+ // first install before the app opens. Keeps the widget from rendering
68
+ // blank in the gallery preview.
69
+ val defaults = defaultStrings()
70
+ val greeting = storedGreeting.ifEmpty { defaults.greeting }
71
+ val title = storedTitle.ifEmpty { defaults.hello }
72
+
73
+ val size = LocalSize.current
74
+ val isSmall = size.width < 200.dp
75
+ // Heuristic: tall enough to fit greeting + title + quote without crowding.
76
+ val isLarge = size.height >= 280.dp
46
77
 
47
- val bgColor = Color(red = 0.08f, green = 0.03f, blue = 0.16f)
48
78
  val white = Color.White
49
79
  val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
80
+ val whiteQuote = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.65f)
50
81
  val gold = Color(red = 1f, green = 0.84f, blue = 0f)
51
- val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.40f)
82
+ val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.45f)
52
83
 
84
+ // The gradient lives in its own Image at the bottom of the stack rather
85
+ // than as `.background(ImageProvider(...))`, because in some Glance
86
+ // versions the latter ends up rendered ABOVE the content — the widget
87
+ // shows just the gradient, no text. The explicit Image+Column layering
88
+ // here is deterministic.
53
89
  Box(
54
- modifier = GlanceModifier.fillMaxSize().background(bgColor).padding(16.dp),
55
- contentAlignment = Alignment.TopStart,
90
+ modifier = GlanceModifier
91
+ .fillMaxSize()
92
+ .clickable(actionStartActivity(launchAppIntent(context))),
56
93
  ) {
57
- Column(modifier = GlanceModifier.fillMaxSize()) {
94
+ Image(
95
+ provider = ImageProvider(R.drawable.widget_gradient_inner),
96
+ contentDescription = null,
97
+ contentScale = ContentScale.FillBounds,
98
+ modifier = GlanceModifier.fillMaxSize(),
99
+ )
100
+ Column(
101
+ modifier = GlanceModifier.fillMaxSize().padding(16.dp),
102
+ ) {
58
103
  Text(
59
104
  text = greeting,
60
105
  style = TextStyle(
@@ -67,21 +112,126 @@ class MyWidgetWidget : GlanceAppWidget() {
67
112
  text = title,
68
113
  style = TextStyle(
69
114
  color = ColorProvider(white),
70
- fontSize = 22.sp,
115
+ fontSize = if (isSmall) 20.sp else 24.sp,
71
116
  fontWeight = FontWeight.Bold,
72
117
  ),
73
118
  modifier = GlanceModifier.padding(top = 4.dp),
74
119
  )
120
+ if (isLarge && quote.isNotEmpty()) {
121
+ Text(
122
+ text = quote,
123
+ style = TextStyle(
124
+ color = ColorProvider(whiteQuote),
125
+ fontSize = 15.sp,
126
+ fontWeight = FontWeight.Normal,
127
+ fontStyle = FontStyle.Italic,
128
+ ),
129
+ maxLines = 4,
130
+ modifier = GlanceModifier.padding(top = 12.dp),
131
+ )
132
+ }
75
133
  Spacer(modifier = GlanceModifier.defaultWeight())
134
+ Row(
135
+ modifier = GlanceModifier.fillMaxWidth(),
136
+ verticalAlignment = Alignment.CenterVertically,
137
+ ) {
138
+ // Empty planText hides the pill (used in logged-out state).
139
+ if (planText.isNotEmpty()) {
140
+ PlanPill(
141
+ isPro = isPro,
142
+ planText = planText,
143
+ gold = gold,
144
+ whiteVerySubtle = whiteVerySubtle,
145
+ )
146
+ }
147
+ // Small intentionally drops the "+" so the layout breathes —
148
+ // the pill sits flush left like the original design.
149
+ if (!isSmall) {
150
+ Spacer(modifier = GlanceModifier.defaultWeight())
151
+ Image(
152
+ provider = ImageProvider(R.drawable.widget_add_button),
153
+ contentDescription = "Add",
154
+ modifier = GlanceModifier
155
+ .size(34.dp)
156
+ .clickable(actionStartActivity(launchAppIntent(context))),
157
+ )
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ @Composable
165
+ private fun PlanPill(
166
+ isPro: Boolean,
167
+ planText: String,
168
+ gold: Color,
169
+ whiteVerySubtle: Color,
170
+ ) {
171
+ if (isPro) {
172
+ Box(
173
+ modifier = GlanceModifier
174
+ .background(ImageProvider(R.drawable.widget_pro_pill_bg))
175
+ .padding(horizontal = 10.dp, vertical = 5.dp),
176
+ ) {
177
+ Text(
178
+ text = "⭐ $planText",
179
+ style = TextStyle(
180
+ color = ColorProvider(gold),
181
+ fontSize = 11.sp,
182
+ fontWeight = FontWeight.Bold,
183
+ ),
184
+ )
185
+ }
186
+ } else {
187
+ Box(
188
+ modifier = GlanceModifier
189
+ .background(ImageProvider(R.drawable.widget_plan_pill_bg))
190
+ .padding(horizontal = 10.dp, vertical = 5.dp),
191
+ ) {
76
192
  Text(
77
- text = if (isPro) "⭐ $planText" else planText,
193
+ text = planText,
78
194
  style = TextStyle(
79
- color = ColorProvider(if (isPro) gold else whiteVerySubtle),
195
+ color = ColorProvider(whiteVerySubtle),
80
196
  fontSize = 11.sp,
81
- fontWeight = if (isPro) FontWeight.Bold else FontWeight.Medium,
197
+ fontWeight = FontWeight.Medium,
82
198
  ),
83
199
  )
84
200
  }
85
201
  }
86
202
  }
203
+
204
+ private data class DefaultStrings(val greeting: String, val hello: String)
205
+
206
+ private fun defaultStrings(): DefaultStrings {
207
+ val hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY)
208
+ val lang = Locale.getDefault().language
209
+ val morning: String; val afternoon: String; val evening: String; val hello: String
210
+ when (lang) {
211
+ "pt" -> { morning = "Bom dia"; afternoon = "Boa tarde"; evening = "Boa noite"; hello = "Olá!" }
212
+ "es" -> { morning = "Buenos días"; afternoon = "Buenas tardes"; evening = "Buenas noches"; hello = "¡Hola!" }
213
+ else -> { morning = "Good morning"; afternoon = "Good afternoon"; evening = "Good evening"; hello = "Hi there!" }
214
+ }
215
+ val greeting = when {
216
+ hour < 12 -> morning
217
+ hour < 18 -> afternoon
218
+ else -> evening
219
+ }
220
+ return DefaultStrings(greeting, hello)
221
+ }
222
+
223
+ /// Builds the exact Intent the system launcher fires when the user taps
224
+ /// the app icon. We must NOT add extra flags here — getLaunchIntentForPackage
225
+ /// already returns `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_IF_NEEDED`,
226
+ /// which is the same combo the launcher uses. Adding `CLEAR_TOP` destroys
227
+ /// go_router's navigation stack on warm starts and lands the user on the
228
+ /// errorBuilder ("404 - Page not found").
229
+ private fun launchAppIntent(context: Context): Intent {
230
+ return context.packageManager.getLaunchIntentForPackage(context.packageName)
231
+ ?: Intent(context, MainActivity::class.java).apply {
232
+ action = Intent.ACTION_MAIN
233
+ addCategory(Intent.CATEGORY_LAUNCHER)
234
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
235
+ }
236
+ }
87
237
  }
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="34dp"
4
+ android:height="34dp"
5
+ android:viewportWidth="34"
6
+ android:viewportHeight="34">
7
+ <path
8
+ android:pathData="M17,0 A17,17 0 1,0 17,34 A17,17 0 1,0 17,0 Z"
9
+ android:fillColor="#2EFFFFFF"/>
10
+ <path
11
+ android:pathData="M17,9 L17,25 M9,17 L25,17"
12
+ android:strokeColor="#FFFFFFFF"
13
+ android:strokeWidth="2.5"
14
+ android:strokeLineCap="round"/>
15
+ </vector>
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
3
+ <gradient
4
+ android:angle="315"
5
+ android:startColor="#FF140829"
6
+ android:endColor="#FF33176B"
7
+ android:type="linear" />
8
+ <corners android:radius="24dp" />
9
+ </shape>