kasy-cli 1.4.2 → 1.5.1

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.
@@ -191,9 +191,16 @@ async function promptOrganizationIfNeeded(tr, onCancel) {
191
191
 
192
192
  function printBanner(tr) {
193
193
  const bar = kleur.gray('─────────────────────────────────────────────────');
194
- const brand = gradient(['#a78bfa', '#60a5fa'])('kasy');
194
+ const logo = [
195
+ ' ╦╔═ ╔═╗ ╔═╗ ╦ ╦',
196
+ ' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝',
197
+ ' ╩ ╩ ╩ ╩ ╚═╝ ╩ ',
198
+ ]
199
+ .map((line) => gradient(['#a78bfa', '#60a5fa'])(line))
200
+ .join('\n');
195
201
  console.log(`\n${bar}\n`);
196
- console.log(` ⚡ ${kleur.bold(brand)}`);
202
+ console.log(logo);
203
+ console.log('');
197
204
  console.log(` ${kleur.dim(tr('new.subtitle2'))}`);
198
205
  console.log(`\n${bar}\n`);
199
206
  }
@@ -12,6 +12,7 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
12
12
  const {
13
13
  AVAILABLE_FEATURES,
14
14
  BASE_COMPONENT_FILES,
15
+ CORE_FILES,
15
16
  CORE_SOURCE_DIR,
16
17
  FEATURES_PATCH_DIR,
17
18
  normalizeFeature,
@@ -29,6 +30,7 @@ const NEEDS_BUILD_RUNNER = [
29
30
  'revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback',
30
31
  ];
31
32
  const COMPONENTS_UPDATE_TARGET = 'components';
33
+ const CORE_UPDATE_TARGET = 'core';
32
34
  const IOS_RELEASE_UPDATE_TARGET = 'ios-release';
33
35
 
34
36
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -80,6 +82,10 @@ function normalizeUpdateTarget(value) {
80
82
  if (componentAliases.has(normalized)) {
81
83
  return COMPONENTS_UPDATE_TARGET;
82
84
  }
85
+ const coreAliases = new Set(['core', 'core_files', 'corefiles']);
86
+ if (coreAliases.has(normalized)) {
87
+ return CORE_UPDATE_TARGET;
88
+ }
83
89
  const iosAliases = new Set(['ios_release', 'iosrelease']);
84
90
  if (iosAliases.has(normalized) || normalized === 'ios-release') {
85
91
  return IOS_RELEASE_UPDATE_TARGET;
@@ -87,9 +93,9 @@ function normalizeUpdateTarget(value) {
87
93
  return null;
88
94
  }
89
95
 
90
- async function applyBaseComponents(projectDir) {
96
+ async function applyFileList(fileList, projectDir) {
91
97
  let filesApplied = 0;
92
- for (const relativePath of BASE_COMPONENT_FILES) {
98
+ for (const relativePath of fileList) {
93
99
  const sourcePath = path.join(CORE_SOURCE_DIR, relativePath);
94
100
  if (!(await fs.pathExists(sourcePath))) {
95
101
  continue;
@@ -102,6 +108,14 @@ async function applyBaseComponents(projectDir) {
102
108
  return filesApplied;
103
109
  }
104
110
 
111
+ function applyBaseComponents(projectDir) {
112
+ return applyFileList(BASE_COMPONENT_FILES, projectDir);
113
+ }
114
+
115
+ function applyCoreFiles(projectDir) {
116
+ return applyFileList(CORE_FILES, projectDir);
117
+ }
118
+
105
119
  /** Same detection logic used by add.js and remove.js. */
106
120
  async function getActiveModules(kitSetup, projectDir) {
107
121
  const modules = [];
@@ -203,6 +217,49 @@ async function runUpdate(module, options = {}) {
203
217
  return;
204
218
  }
205
219
 
220
+ if (normalizedTarget === CORE_UPDATE_TARGET) {
221
+ if (!options.yes) {
222
+ console.log(kleur.yellow(`\n ⚠ ${t('update.warn.commitComponents')}\n`));
223
+ const { confirmed } = await prompts(
224
+ { type: 'confirm', name: 'confirmed', message: t('update.confirmCore'), initial: false },
225
+ { onCancel: () => { throw new Error(t('update.cancelled')); } }
226
+ );
227
+ if (!confirmed) {
228
+ console.log(kleur.dim(`\n${t('update.cancelled')}\n`));
229
+ return;
230
+ }
231
+ }
232
+
233
+ console.log('');
234
+ const spinner = ora(t('update.applyingCore')).start();
235
+ try {
236
+ const filesApplied = await applyCoreFiles(projectDir);
237
+ if (filesApplied === 0) {
238
+ spinner.warn(t('update.noComponentFiles'));
239
+ return;
240
+ }
241
+ spinner.succeed(t('update.appliedCore', { count: filesApplied }));
242
+ } catch (err) {
243
+ spinner.fail(t('update.applyComponentsFailed'));
244
+ throw err;
245
+ }
246
+
247
+ {
248
+ const spinnerPubGet = ora(t('update.pubGet')).start();
249
+ try {
250
+ await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
251
+ spinnerPubGet.succeed(t('update.pubGetDone'));
252
+ } catch {
253
+ spinnerPubGet.warn(t('update.pubGetFailed'));
254
+ }
255
+ }
256
+
257
+ kitSetup.cliVersion = currentVersion;
258
+ await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
259
+ console.log(kleur.green(`\n✓ ${t('update.coreSuccess')}\n`));
260
+ return;
261
+ }
262
+
206
263
  if (normalizedTarget === IOS_RELEASE_UPDATE_TARGET) {
207
264
  const patchDir = path.join(FEATURES_PATCH_DIR, 'ios-release');
208
265
  if (!(await fs.pathExists(patchDir))) {
@@ -249,7 +306,7 @@ async function runUpdate(module, options = {}) {
249
306
  throw new Error(
250
307
  t('update.error.unknownTarget', {
251
308
  module,
252
- list: [...AVAILABLE_FEATURES, COMPONENTS_UPDATE_TARGET, IOS_RELEASE_UPDATE_TARGET].join(', '),
309
+ list: [...AVAILABLE_FEATURES, COMPONENTS_UPDATE_TARGET, CORE_UPDATE_TARGET, IOS_RELEASE_UPDATE_TARGET].join(', '),
253
310
  })
254
311
  );
255
312
  }
@@ -335,7 +392,7 @@ async function runUpdate(module, options = {}) {
335
392
 
336
393
  // ── Mode B: show status ──────────────────────────────────────────────────────
337
394
  const alreadyUpToDate = projectVersion && !isNewer(currentVersion, projectVersion);
338
- const modulesForChangelog = [...new Set([...activeModules, COMPONENTS_UPDATE_TARGET])];
395
+ const modulesForChangelog = [...new Set([...activeModules, COMPONENTS_UPDATE_TARGET, CORE_UPDATE_TARGET])];
339
396
  const changes = getChangesSince(changelog, projectVersion, modulesForChangelog, options.language);
340
397
 
341
398
  // Modules in this project that have patch dirs (can be re-applied)
@@ -1,10 +1,10 @@
1
1
  {
2
- "1.4.0": {
2
+ "1.5.0": {
3
3
  "modules": {
4
- "components": {
5
- "pt": "Melhorias no WebDevicePreview: minimizar/maximizar, orientação, locale e screenshot",
6
- "en": "WebDevicePreview improvements: minimize/maximize, orientation, locale and screenshot",
7
- "es": "Mejoras en WebDevicePreview: minimizar/maximizar, orientación, locale y captura de pantalla"
4
+ "widget": {
5
+ "pt": "Widget redesenhado: saudação por horário no idioma do usuário (pt/en/es), nome real e status do plano",
6
+ "en": "Redesigned widget: time-of-day greeting in the user's language (pt/en/es), real name and plan status",
7
+ "es": "Widget rediseñado: saludo según el horario en el idioma del usuario (pt/en/es), nombre real y estado del plan"
8
8
  }
9
9
  }
10
10
  }
@@ -74,6 +74,54 @@ const DEFAULT_FEATURES = [...AVAILABLE_FEATURES];
74
74
  * shared widgets); keep this list in sync with `Firebase/lib/components/` and
75
75
  * the home component catalog.
76
76
  */
77
+ const CORE_FILES = [
78
+ // Dev tools
79
+ 'lib/core/web_device_preview/web_device_preview.dart',
80
+ 'lib/core/dev_inspector/dev_inspector.dart',
81
+ 'lib/core/dev_inspector/dev_inspector_highlight.dart',
82
+ 'lib/core/dev_inspector/dev_inspector_info.dart',
83
+ 'lib/core/dev_inspector/dev_inspector_panel.dart',
84
+ 'lib/core/dev_inspector/dev_inspector_service.dart',
85
+ 'lib/core/keyboard_fix/keyboard_flicker_fix.dart',
86
+ // Animations
87
+ 'lib/core/animations/bottomfade_anim.dart',
88
+ 'lib/core/animations/movefade_anim.dart',
89
+ 'lib/core/animations/slideright_anim.dart',
90
+ // Core widgets (pure utilities, never user-modified)
91
+ 'lib/core/widgets/debouncer.dart',
92
+ 'lib/core/widgets/kasy_hover.dart',
93
+ 'lib/core/widgets/kasy_pressable.dart',
94
+ 'lib/core/widgets/kasy_scroll_behavior.dart',
95
+ 'lib/core/widgets/keyboard_visibility.dart',
96
+ 'lib/core/widgets/page_background.dart',
97
+ 'lib/core/widgets/page_not_found.dart',
98
+ 'lib/core/widgets/responsive_layout.dart',
99
+ 'lib/core/widgets/update_bottom_sheet.dart',
100
+ // Page transitions (pure utilities)
101
+ 'lib/core/navigation/kasy_fade_page_transitions_builder.dart',
102
+ 'lib/core/navigation/kasy_material_page_route.dart',
103
+ 'lib/core/navigation/kasy_page_transition.dart',
104
+ 'lib/core/navigation/kasy_route_transition.dart',
105
+ 'lib/core/navigation/kasy_transition_kind.dart',
106
+ // Utilities
107
+ 'lib/core/toast/toast_service.dart',
108
+ 'lib/core/ui/app_dialog.dart',
109
+ 'lib/core/icons/kasy_icons.dart',
110
+ 'lib/core/haptics/kasy_haptics.dart',
111
+ 'lib/core/haptics/haptic_feedback_notifier.dart',
112
+ // Theme (colors.dart and shadows.dart are in BASE_COMPONENT_FILES)
113
+ 'lib/core/theme/spacing.dart',
114
+ 'lib/core/theme/texts.dart',
115
+ 'lib/core/theme/radius.dart',
116
+ 'lib/core/theme/theme.dart',
117
+ 'lib/core/theme/theme_data/theme_data.dart',
118
+ 'lib/core/theme/theme_data/theme_data_factory.dart',
119
+ 'lib/core/theme/extensions/theme_extension.dart',
120
+ 'lib/core/theme/providers/kasy_theme.dart',
121
+ 'lib/core/theme/providers/theme_provider.dart',
122
+ 'lib/core/theme/universal_theme.dart',
123
+ ];
124
+
77
125
  const BASE_COMPONENT_FILES = [
78
126
  'lib/core/theme/colors.dart',
79
127
  'lib/core/theme/shadows.dart',
@@ -170,6 +218,7 @@ module.exports = {
170
218
  AVAILABLE_FEATURES,
171
219
  DEFAULT_FEATURES,
172
220
  BASE_COMPONENT_FILES,
221
+ CORE_FILES,
173
222
  normalizeBackend,
174
223
  normalizeFeature,
175
224
  parseFeatureList,
@@ -1,7 +1,10 @@
1
+ import 'dart:io';
2
+
1
3
  import 'package:home_widget/home_widget.dart';
2
- import 'package:logger/logger.dart';
4
+ import 'package:kasy_kit/core/data/models/user.dart';
3
5
  import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
4
6
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
+ import 'package:logger/logger.dart';
5
8
  import 'package:riverpod_annotation/riverpod_annotation.dart';
6
9
 
7
10
  part 'home_widget_mywidget_service.g.dart';
@@ -18,40 +21,78 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
18
21
  @override
19
22
  Future<void> update() {
20
23
  Logger().i('🔄 Updating MyWidget Home Widget');
21
- // Example: fetch some data from the user state
22
- final userState = ref.read(userStateNotifierProvider);
23
- final userName = userState.user.idOrNull ?? 'Guest';
24
+ final user = ref.read(userStateNotifierProvider).user;
25
+
26
+ final name = switch (user) {
27
+ AuthenticatedUserData(:final name)
28
+ when name != null && name.isNotEmpty =>
29
+ name.split(' ').first,
30
+ _ => 'there',
31
+ };
32
+
33
+ final isPro = switch (user) {
34
+ AuthenticatedUserData(:final subscription) ||
35
+ AnonymousUserData(:final subscription) =>
36
+ subscription?.isActive ?? false,
37
+ _ => false,
38
+ };
24
39
 
25
- return updateWidget({'title': 'Hello, $userName', 'counter': '0'});
40
+ return updateWidget({
41
+ 'greeting': _greeting(),
42
+ 'name': name,
43
+ 'isPro': isPro.toString(),
44
+ });
26
45
  }
27
46
 
28
- /// Update widget data
29
- ///
30
- /// This will save data that the widget can read
31
47
  Future<void> updateWidget(Map<String, String> data) async {
32
- await HomeWidget.saveWidgetData<String>('title', data['title'] ?? 'Hello');
48
+ await HomeWidget.saveWidgetData<String>(
49
+ 'greeting', data['greeting'] ?? 'Good morning');
50
+ await HomeWidget.saveWidgetData<String>('name', data['name'] ?? 'there');
51
+ await HomeWidget.saveWidgetData<String>(
52
+ 'isPro', data['isPro'] ?? 'false');
33
53
 
34
- await HomeWidget.saveWidgetData<String>('counter', data['counter'] ?? '0');
35
-
36
- // Trigger widget update
37
54
  await HomeWidget.updateWidget(
38
55
  name: _androidWidgetName,
39
56
  iOSName: _iosWidgetName,
40
57
  );
41
58
  }
42
59
 
43
- /// Get current widget data
44
60
  Future<Map<String, dynamic>> getWidgetData() async {
45
61
  return {
46
- 'title': await HomeWidget.getWidgetData<String>(
47
- 'title',
48
- defaultValue: 'Hello',
62
+ 'greeting': await HomeWidget.getWidgetData<String>(
63
+ 'greeting',
64
+ defaultValue: 'Good morning',
49
65
  ),
50
-
51
- 'counter': await HomeWidget.getWidgetData<String>(
52
- 'counter',
53
- defaultValue: '0',
66
+ 'name': await HomeWidget.getWidgetData<String>(
67
+ 'name',
68
+ defaultValue: 'there',
54
69
  ),
70
+ 'isPro': await HomeWidget.getWidgetData<String>(
71
+ 'isPro',
72
+ defaultValue: 'false',
73
+ ),
74
+ };
75
+ }
76
+
77
+ // Returns a time-of-day greeting in the device language (pt / es / en).
78
+ // Uses Platform.localeName so it works in background isolates without
79
+ // requiring the Flutter locale system to be initialized.
80
+ static String _greeting() {
81
+ final lang = Platform.localeName.split(RegExp(r'[_\-]')).first.toLowerCase();
82
+ final hour = DateTime.now().hour;
83
+
84
+ return switch (lang) {
85
+ 'pt' => hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite',
86
+ 'es' => hour < 12
87
+ ? 'Buenos días'
88
+ : hour < 18
89
+ ? 'Buenas tardes'
90
+ : 'Buenas noches',
91
+ _ => hour < 12
92
+ ? 'Good morning'
93
+ : hour < 18
94
+ ? 'Good afternoon'
95
+ : 'Good evening',
55
96
  };
56
97
  }
57
98
  }
package/lib/utils/i18n.js CHANGED
@@ -698,11 +698,14 @@ const MESSAGES = {
698
698
  'update.warn.commitComponents': 'This will overwrite base component files. Make sure you have committed your changes first.',
699
699
  'update.confirm': 'Overwrite feature "{module}" files with the latest version?',
700
700
  'update.confirmComponents': 'Overwrite base component files with the latest version?',
701
+ 'update.confirmCore': 'Overwrite core files (animations, widgets, theme, dev tools) with the latest version?',
701
702
  'update.cancelled': 'Cancelled.',
702
703
  'update.applying': 'Applying update for feature: {module}',
703
704
  'update.applyingComponents': 'Applying update for base components...',
705
+ 'update.applyingCore': 'Applying update for core files...',
704
706
  'update.applied': 'Feature {module} updated',
705
707
  'update.appliedComponents': '{count} base component files updated',
708
+ 'update.appliedCore': '{count} core files updated',
706
709
  'update.applyFailed': 'Failed to apply update for feature {module}',
707
710
  'update.applyComponentsFailed': 'Failed to apply update for base components',
708
711
  'update.noPatch': 'Feature "{module}" has no files to update (configuration-only feature).',
@@ -715,6 +718,7 @@ const MESSAGES = {
715
718
  'update.buildRunnerFailed': 'build_runner failed — run it manually',
716
719
  'update.success': 'Feature "{module}" updated successfully.',
717
720
  'update.componentsSuccess': 'Base components updated successfully.',
721
+ 'update.coreSuccess': 'Core files updated successfully.',
718
722
  },
719
723
  pt: {
720
724
  'cli.tagline': 'Crie apps móveis sem dor de configuração',
@@ -1409,11 +1413,14 @@ const MESSAGES = {
1409
1413
  'update.warn.commitComponents': 'Isso vai sobrescrever arquivos dos componentes base. Faca commit de tudo antes de continuar.',
1410
1414
  'update.confirm': 'Sobrescrever arquivos da feature "{module}" com a versao mais recente?',
1411
1415
  'update.confirmComponents': 'Sobrescrever arquivos dos componentes base com a versao mais recente?',
1416
+ 'update.confirmCore': 'Sobrescrever arquivos do core (animacoes, widgets, tema, ferramentas de dev) com a versao mais recente?',
1412
1417
  'update.cancelled': 'Cancelado.',
1413
1418
  'update.applying': 'Aplicando atualizacao da feature: {module}',
1414
1419
  'update.applyingComponents': 'Aplicando atualizacao dos componentes base...',
1420
+ 'update.applyingCore': 'Aplicando atualizacao dos arquivos de core...',
1415
1421
  'update.applied': 'Feature {module} atualizada',
1416
1422
  'update.appliedComponents': '{count} arquivos de componentes base atualizados',
1423
+ 'update.appliedCore': '{count} arquivos de core atualizados',
1417
1424
  'update.applyFailed': 'Falha ao aplicar atualizacao da feature {module}',
1418
1425
  'update.applyComponentsFailed': 'Falha ao aplicar atualizacao dos componentes base',
1419
1426
  'update.noPatch': 'Feature "{module}" nao tem arquivos para atualizar (feature so de configuracao).',
@@ -1426,6 +1433,7 @@ const MESSAGES = {
1426
1433
  'update.buildRunnerFailed': 'build_runner falhou — execute manualmente',
1427
1434
  'update.success': 'Feature "{module}" atualizada com sucesso.',
1428
1435
  'update.componentsSuccess': 'Componentes base atualizados com sucesso.',
1436
+ 'update.coreSuccess': 'Arquivos de core atualizados com sucesso.',
1429
1437
  },
1430
1438
  es: {
1431
1439
  'cli.tagline': 'Crea apps móviles sin dolor de configuración',
@@ -2120,11 +2128,14 @@ const MESSAGES = {
2120
2128
  'update.warn.commitComponents': 'Esto sobreescribira archivos de los componentes base. Asegurate de haber hecho commit antes.',
2121
2129
  'update.confirm': 'Sobreescribir archivos de la feature "{module}" con la version mas reciente?',
2122
2130
  'update.confirmComponents': 'Sobreescribir archivos de los componentes base con la version mas reciente?',
2131
+ 'update.confirmCore': 'Sobreescribir archivos de core (animaciones, widgets, tema, herramientas dev) con la version mas reciente?',
2123
2132
  'update.cancelled': 'Cancelado.',
2124
2133
  'update.applying': 'Aplicando actualizacion de la feature: {module}',
2125
2134
  'update.applyingComponents': 'Aplicando actualizacion de componentes base...',
2135
+ 'update.applyingCore': 'Aplicando actualizacion de archivos de core...',
2126
2136
  'update.applied': 'Feature {module} actualizada',
2127
2137
  'update.appliedComponents': '{count} archivos de componentes base actualizados',
2138
+ 'update.appliedCore': '{count} archivos de core actualizados',
2128
2139
  'update.applyFailed': 'Error al aplicar actualizacion de la feature {module}',
2129
2140
  'update.applyComponentsFailed': 'Error al aplicar actualizacion de componentes base',
2130
2141
  'update.noPatch': 'La feature "{module}" no tiene archivos para actualizar (feature solo de configuracion).',
@@ -2137,6 +2148,7 @@ const MESSAGES = {
2137
2148
  'update.buildRunnerFailed': 'build_runner fallo — ejecutalo manualmente',
2138
2149
  'update.success': 'Feature "{module}" actualizada exitosamente.',
2139
2150
  'update.componentsSuccess': 'Componentes base actualizados exitosamente.',
2151
+ 'update.coreSuccess': 'Archivos de core actualizados exitosamente.',
2140
2152
  }
2141
2153
  };
2142
2154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
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"
@@ -1,22 +1,29 @@
1
1
  package com.aicrus.firebase.kit
2
2
 
3
+ import android.content.Context
3
4
  import androidx.compose.runtime.Composable
4
5
  import androidx.compose.ui.graphics.Color
5
6
  import androidx.compose.ui.unit.dp
7
+ import androidx.compose.ui.unit.sp
6
8
  import androidx.glance.GlanceId
7
9
  import androidx.glance.GlanceModifier
8
10
  import androidx.glance.appwidget.GlanceAppWidget
9
11
  import androidx.glance.appwidget.provideContent
10
12
  import androidx.glance.background
13
+ import androidx.glance.currentState
14
+ import androidx.glance.layout.Alignment
11
15
  import androidx.glance.layout.Box
12
16
  import androidx.glance.layout.Column
17
+ import androidx.glance.layout.Spacer
18
+ import androidx.glance.layout.fillMaxSize
13
19
  import androidx.glance.layout.padding
14
20
  import androidx.glance.state.GlanceStateDefinition
21
+ import androidx.glance.text.FontWeight
15
22
  import androidx.glance.text.Text
23
+ import androidx.glance.text.TextStyle
24
+ import androidx.glance.unit.ColorProvider
16
25
  import es.antonborri.home_widget.HomeWidgetGlanceState
17
26
  import es.antonborri.home_widget.HomeWidgetGlanceStateDefinition
18
- import android.content.Context
19
- import androidx.glance.currentState
20
27
 
21
28
  class MyWidgetWidget : GlanceAppWidget() {
22
29
 
@@ -32,18 +39,47 @@ class MyWidgetWidget : GlanceAppWidget() {
32
39
  @Composable
33
40
  private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
34
41
  val prefs = currentState.preferences
42
+ val greeting = prefs.getString("greeting", "Good morning") ?: "Good morning"
43
+ val name = prefs.getString("name", "there") ?: "there"
44
+ val isPro = prefs.getString("isPro", "false") == "true"
35
45
 
36
- val title = prefs.getString("title", "Hello")
37
-
38
- val counter = prefs.getString("counter", "0")
39
-
40
- Box(modifier = GlanceModifier.background(Color.White).padding(16.dp)) {
41
- Column {
42
-
43
- Text(text = title ?: "--", modifier = GlanceModifier.padding(bottom = 4.dp))
44
-
45
- Text(text = counter ?: "--")
46
+ val bgColor = Color(red = 0.08f, green = 0.03f, blue = 0.16f)
47
+ val white = Color.White
48
+ val whiteSubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.55f)
49
+ val gold = Color(red = 1f, green = 0.84f, blue = 0f)
50
+ val whiteVerySubtle = Color(red = 1f, green = 1f, blue = 1f, alpha = 0.40f)
46
51
 
52
+ Box(
53
+ modifier = GlanceModifier.fillMaxSize().background(bgColor).padding(16.dp),
54
+ contentAlignment = Alignment.TopStart,
55
+ ) {
56
+ Column(modifier = GlanceModifier.fillMaxSize()) {
57
+ Text(
58
+ text = greeting,
59
+ style = TextStyle(
60
+ color = ColorProvider(whiteSubtle),
61
+ fontSize = 11.sp,
62
+ fontWeight = FontWeight.Medium,
63
+ ),
64
+ )
65
+ Text(
66
+ text = "Hi, $name!",
67
+ style = TextStyle(
68
+ color = ColorProvider(white),
69
+ fontSize = 22.sp,
70
+ fontWeight = FontWeight.Bold,
71
+ ),
72
+ modifier = GlanceModifier.padding(top = 4.dp),
73
+ )
74
+ Spacer(modifier = GlanceModifier.defaultWeight())
75
+ Text(
76
+ text = if (isPro) "⭐ PRO" else "Free plan",
77
+ style = TextStyle(
78
+ color = ColorProvider(if (isPro) gold else whiteVerySubtle),
79
+ fontSize = 11.sp,
80
+ fontWeight = if (isPro) FontWeight.Bold else FontWeight.Medium,
81
+ ),
82
+ )
47
83
  }
48
84
  }
49
85
  }
@@ -4,13 +4,17 @@
4
4
  "name": "MyWidget",
5
5
  "description": "Sample home widget generated by kasy",
6
6
  "metadata": {
7
- "title": {
7
+ "greeting": {
8
8
  "type": "string",
9
- "defaultValue": "Hello"
9
+ "defaultValue": "Good morning"
10
10
  },
11
- "counter": {
12
- "type": "number",
13
- "defaultValue": "0"
11
+ "name": {
12
+ "type": "string",
13
+ "defaultValue": "there"
14
+ },
15
+ "isPro": {
16
+ "type": "string",
17
+ "defaultValue": "false"
14
18
  }
15
19
  },
16
20
  "isLockScreenWidget": false,
@@ -3,63 +3,78 @@ import SwiftUI
3
3
 
4
4
  struct MyWidgetProvider: TimelineProvider {
5
5
  func placeholder(in context: Context) -> MyWidgetEntry {
6
- MyWidgetEntry(
7
- date: Date(),
8
- title: "Hello",
9
- counter: "0"
10
- )
6
+ MyWidgetEntry(date: Date(), greeting: "Good morning", name: "there", isPro: false)
11
7
  }
12
8
 
13
- func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> ()) {
9
+ func getSnapshot(in context: Context, completion: @escaping (MyWidgetEntry) -> Void) {
14
10
  let prefs = UserDefaults(suiteName: "group.com.aicrus.firebase.kit")
15
-
16
- let title = prefs?.string(forKey: "title") ?? "Hello"
17
-
18
- let counter = prefs?.string(forKey: "counter") ?? "0"
19
-
20
- let entry = MyWidgetEntry(
11
+ completion(MyWidgetEntry(
21
12
  date: Date(),
22
- title: title,
23
- counter: counter
24
- )
25
- completion(entry)
13
+ greeting: prefs?.string(forKey: "greeting") ?? "Good morning",
14
+ name: prefs?.string(forKey: "name") ?? "there",
15
+ isPro: prefs?.string(forKey: "isPro") == "true"
16
+ ))
26
17
  }
27
18
 
28
- func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> ()) {
29
- getSnapshot(in: context) { (entry) in
30
- let timeline = Timeline(entries: [entry], policy: .atEnd)
31
- completion(timeline)
19
+ func getTimeline(in context: Context, completion: @escaping (Timeline<MyWidgetEntry>) -> Void) {
20
+ getSnapshot(in: context) { entry in
21
+ completion(Timeline(entries: [entry], policy: .atEnd))
32
22
  }
33
23
  }
34
24
  }
35
25
 
36
26
  struct MyWidgetEntry: TimelineEntry {
37
27
  let date: Date
38
-
39
- let title: String
40
-
41
- let counter: String
42
-
28
+ let greeting: String
29
+ let name: String
30
+ let isPro: Bool
43
31
  }
44
32
 
45
33
  struct MyWidgetWidgetView: View {
46
34
  var entry: MyWidgetProvider.Entry
35
+ @Environment(\.widgetFamily) var family
36
+
37
+ private var titleSize: CGFloat {
38
+ switch family {
39
+ case .systemSmall: return 24
40
+ case .systemMedium: return 28
41
+ default: return 34
42
+ }
43
+ }
47
44
 
48
45
  var body: some View {
49
- VStack(alignment: .leading, spacing: 8) {
50
- Text(entry.title)
51
- .font(.system(size: 16, weight: .semibold, design: .rounded))
52
- .foregroundColor(.primary)
53
- .multilineTextAlignment(.leading)
54
- .lineLimit(2)
46
+ VStack(alignment: .leading, spacing: 0) {
47
+ Text(entry.greeting)
48
+ .font(.system(size: 11, weight: .medium, design: .rounded))
49
+ .foregroundStyle(.white.opacity(0.55))
50
+ .lineLimit(1)
55
51
 
56
- Text(entry.counter)
57
- .font(.system(size: 24, weight: .bold, design: .rounded))
58
- .foregroundColor(.primary)
52
+ Spacer().frame(height: 6)
59
53
 
54
+ Text("Hi, \(entry.name)!")
55
+ .font(.system(size: titleSize, weight: .bold, design: .rounded))
56
+ .foregroundStyle(.white)
57
+ .lineLimit(2)
58
+ .minimumScaleFactor(0.75)
59
+
60
+ Spacer()
61
+
62
+ if entry.isPro {
63
+ Label("PRO", systemImage: "star.fill")
64
+ .font(.system(size: 10, weight: .bold, design: .rounded))
65
+ .foregroundStyle(Color(red: 1.0, green: 0.84, blue: 0.0))
66
+ .padding(.horizontal, 8)
67
+ .padding(.vertical, 4)
68
+ .background(Color(red: 1.0, green: 0.84, blue: 0.0).opacity(0.18))
69
+ .clipShape(Capsule())
70
+ } else {
71
+ Text("Free plan")
72
+ .font(.system(size: 10, weight: .medium, design: .rounded))
73
+ .foregroundStyle(.white.opacity(0.4))
74
+ }
60
75
  }
61
- .padding()
62
76
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
77
+ .padding()
63
78
  }
64
79
  }
65
80
 
@@ -69,18 +84,31 @@ struct MyWidgetWidget: Widget {
69
84
  var body: some WidgetConfiguration {
70
85
  StaticConfiguration(kind: kind, provider: MyWidgetProvider()) { entry in
71
86
  MyWidgetWidgetView(entry: entry)
72
- .containerBackground(Color(.systemBackground), for: .widget)
87
+ .containerBackground(for: .widget) {
88
+ LinearGradient(
89
+ gradient: Gradient(colors: [
90
+ Color(red: 0.08, green: 0.03, blue: 0.16),
91
+ Color(red: 0.20, green: 0.09, blue: 0.42),
92
+ ]),
93
+ startPoint: .topLeading,
94
+ endPoint: .bottomTrailing
95
+ )
96
+ }
73
97
  }
74
98
  .configurationDisplayName("MyWidget")
75
99
  .description("Sample home widget generated by kasy")
76
- .supportedFamilies([
77
- .systemSmall,.systemMedium,.systemLarge, // test 2
78
- ])
100
+ .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
79
101
  }
80
102
  }
81
103
 
82
- #Preview("MyWidget Widget", as: .systemSmall) {
104
+ #Preview("Small", as: .systemSmall) {
105
+ MyWidgetWidget()
106
+ } timeline: {
107
+ MyWidgetEntry(date: .now, greeting: "Good morning", name: "Paulo", isPro: true)
108
+ }
109
+
110
+ #Preview("Medium", as: .systemMedium) {
83
111
  MyWidgetWidget()
84
112
  } timeline: {
85
- MyWidgetEntry(date: .now, title: "Hello", counter: "0")
113
+ MyWidgetEntry(date: .now, greeting: "Good afternoon", name: "Paulo", isPro: false)
86
114
  }
@@ -1,4 +1,7 @@
1
+ import 'dart:io';
2
+
1
3
  import 'package:home_widget/home_widget.dart';
4
+ import 'package:kasy_kit/core/data/models/user.dart';
2
5
  import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
3
6
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
4
7
  import 'package:logger/logger.dart';
@@ -18,40 +21,78 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
18
21
  @override
19
22
  Future<void> update() {
20
23
  Logger().i('🔄 Updating MyWidget Home Widget');
21
- // Example: fetch some data from the user state
22
- final userState = ref.read(userStateNotifierProvider);
23
- final userName = userState.user.idOrNull ?? 'Guest';
24
+ final user = ref.read(userStateNotifierProvider).user;
25
+
26
+ final name = switch (user) {
27
+ AuthenticatedUserData(:final name)
28
+ when name != null && name.isNotEmpty =>
29
+ name.split(' ').first,
30
+ _ => 'there',
31
+ };
24
32
 
25
- return updateWidget({'title': 'Hello, $userName', 'counter': '0'});
33
+ final isPro = switch (user) {
34
+ AuthenticatedUserData(:final subscription) ||
35
+ AnonymousUserData(:final subscription) =>
36
+ subscription?.isActive ?? false,
37
+ _ => false,
38
+ };
39
+
40
+ return updateWidget({
41
+ 'greeting': _greeting(),
42
+ 'name': name,
43
+ 'isPro': isPro.toString(),
44
+ });
26
45
  }
27
46
 
28
- /// Update widget data
29
- ///
30
- /// This will save data that the widget can read
31
47
  Future<void> updateWidget(Map<String, String> data) async {
32
- await HomeWidget.saveWidgetData<String>('title', data['title'] ?? 'Hello');
33
-
34
- await HomeWidget.saveWidgetData<String>('counter', data['counter'] ?? '0');
48
+ await HomeWidget.saveWidgetData<String>(
49
+ 'greeting', data['greeting'] ?? 'Good morning');
50
+ await HomeWidget.saveWidgetData<String>('name', data['name'] ?? 'there');
51
+ await HomeWidget.saveWidgetData<String>(
52
+ 'isPro', data['isPro'] ?? 'false');
35
53
 
36
- // Trigger widget update
37
54
  await HomeWidget.updateWidget(
38
55
  name: _androidWidgetName,
39
56
  iOSName: _iosWidgetName,
40
57
  );
41
58
  }
42
59
 
43
- /// Get current widget data
44
60
  Future<Map<String, dynamic>> getWidgetData() async {
45
61
  return {
46
- 'title': await HomeWidget.getWidgetData<String>(
47
- 'title',
48
- defaultValue: 'Hello',
62
+ 'greeting': await HomeWidget.getWidgetData<String>(
63
+ 'greeting',
64
+ defaultValue: 'Good morning',
49
65
  ),
50
-
51
- 'counter': await HomeWidget.getWidgetData<String>(
52
- 'counter',
53
- defaultValue: '0',
66
+ 'name': await HomeWidget.getWidgetData<String>(
67
+ 'name',
68
+ defaultValue: 'there',
69
+ ),
70
+ 'isPro': await HomeWidget.getWidgetData<String>(
71
+ 'isPro',
72
+ defaultValue: 'false',
54
73
  ),
55
74
  };
56
75
  }
76
+
77
+ // Returns a time-of-day greeting in the device language (pt / es / en).
78
+ // Uses Platform.localeName so it works in background isolates without
79
+ // requiring the Flutter locale system to be initialized.
80
+ static String _greeting() {
81
+ final lang = Platform.localeName.split(RegExp(r'[_\-]')).first.toLowerCase();
82
+ final hour = DateTime.now().hour;
83
+
84
+ return switch (lang) {
85
+ 'pt' => hour < 12 ? 'Bom dia' : hour < 18 ? 'Boa tarde' : 'Boa noite',
86
+ 'es' => hour < 12
87
+ ? 'Buenos días'
88
+ : hour < 18
89
+ ? 'Buenas tardes'
90
+ : 'Buenas noches',
91
+ _ => hour < 12
92
+ ? 'Good morning'
93
+ : hour < 18
94
+ ? 'Good afternoon'
95
+ : 'Good evening',
96
+ };
97
+ }
57
98
  }