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.
- package/bin/kasy.js +140 -12
- package/lib/commands/add.js +2 -2
- package/lib/commands/codemagic.js +11 -4
- package/lib/commands/deploy.js +3 -3
- package/lib/commands/favicon.js +115 -0
- package/lib/commands/icon.js +143 -0
- package/lib/commands/ios.js +28 -7
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +385 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +14 -4
- package/lib/commands/update.js +1 -1
- package/lib/scaffold/backends/api/patch/README.md +1 -1
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/backends/firebase/tokens.js +2 -2
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
- package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
- package/lib/scaffold/backends/supabase/patch/README.md +1 -1
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
- package/lib/utils/apple-release.js +115 -16
- package/lib/utils/checks.js +45 -107
- package/lib/utils/debug.js +75 -0
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/friendly-error.js +91 -0
- package/lib/utils/i18n/messages-en.js +970 -0
- package/lib/utils/i18n/messages-es.js +968 -0
- package/lib/utils/i18n/messages-pt.js +968 -0
- package/lib/utils/i18n.js +21 -2483
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/png-padding.js +120 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +8 -4
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/android/app/build.gradle.kts +10 -1
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +161 -11
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +53 -0
- package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
- package/templates/firebase/assets/images/favicon.png +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/assets/images/icon_android.png +0 -0
- package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/firestore.indexes.json +10 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
- package/templates/firebase/functions/src/index.ts +1 -0
- package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
- package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Info.plist +2 -2
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +73 -19
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
- package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
- package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
- package/templates/firebase/lib/features/home/home_page.dart +0 -6
- package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
- package/templates/firebase/lib/i18n/en.i18n.json +4 -1
- package/templates/firebase/lib/i18n/es.i18n.json +4 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
- package/templates/firebase/pubspec.yaml +10 -3
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +9 -0
- package/templates/firebase/web/manifest.json +3 -3
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
3
|
+
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Data model
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/// Data model for a single tab item.
|
|
9
|
+
class KasyTabItem {
|
|
10
|
+
final String label;
|
|
11
|
+
final IconData? icon;
|
|
12
|
+
final bool enabled;
|
|
13
|
+
|
|
14
|
+
const KasyTabItem({
|
|
15
|
+
required this.label,
|
|
16
|
+
this.icon,
|
|
17
|
+
this.enabled = true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Enums
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/// Visual style for [KasyTabs].
|
|
26
|
+
enum KasyTabsVariant {
|
|
27
|
+
/// Sliding white pill on a gray container.
|
|
28
|
+
primary,
|
|
29
|
+
|
|
30
|
+
/// Blue underline indicator with a bottom divider.
|
|
31
|
+
secondary,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Sizing mode for [KasyTabs].
|
|
35
|
+
enum KasyTabsMode {
|
|
36
|
+
/// Wraps content (intrinsic width tabs).
|
|
37
|
+
hug,
|
|
38
|
+
|
|
39
|
+
/// Each tab stretches to fill the available width equally.
|
|
40
|
+
fill,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// KasyTabs
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/// Animated tab bar with primary (pill) and secondary (underline) variants.
|
|
48
|
+
///
|
|
49
|
+
/// Usage:
|
|
50
|
+
/// ```dart
|
|
51
|
+
/// KasyTabs(
|
|
52
|
+
/// tabs: ['General', 'Appearance', 'Notifications'],
|
|
53
|
+
/// selectedIndex: _index,
|
|
54
|
+
/// onTabSelected: (i) => setState(() => _index = i),
|
|
55
|
+
/// )
|
|
56
|
+
/// ```
|
|
57
|
+
///
|
|
58
|
+
/// Supports [KasyTabItem] for per-tab icons and disabled state. The short-form
|
|
59
|
+
/// constructor accepts plain [String] labels.
|
|
60
|
+
class KasyTabs extends StatefulWidget {
|
|
61
|
+
/// Plain-string convenience constructor.
|
|
62
|
+
///
|
|
63
|
+
/// Converts each string to a [KasyTabItem] with default settings.
|
|
64
|
+
KasyTabs({
|
|
65
|
+
super.key,
|
|
66
|
+
required List<String> tabs,
|
|
67
|
+
required this.selectedIndex,
|
|
68
|
+
required this.onTabSelected,
|
|
69
|
+
this.variant = KasyTabsVariant.primary,
|
|
70
|
+
this.mode = KasyTabsMode.hug,
|
|
71
|
+
}) : items = tabs.map((l) => KasyTabItem(label: l)).toList();
|
|
72
|
+
|
|
73
|
+
/// Full constructor accepting [KasyTabItem] instances (supports icons/disabled).
|
|
74
|
+
const KasyTabs.items({
|
|
75
|
+
super.key,
|
|
76
|
+
required this.items,
|
|
77
|
+
required this.selectedIndex,
|
|
78
|
+
required this.onTabSelected,
|
|
79
|
+
this.variant = KasyTabsVariant.primary,
|
|
80
|
+
this.mode = KasyTabsMode.hug,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
final List<KasyTabItem> items;
|
|
84
|
+
|
|
85
|
+
/// Index of the currently selected tab.
|
|
86
|
+
final int selectedIndex;
|
|
87
|
+
|
|
88
|
+
/// Called when the user taps an enabled tab.
|
|
89
|
+
final ValueChanged<int> onTabSelected;
|
|
90
|
+
|
|
91
|
+
final KasyTabsVariant variant;
|
|
92
|
+
final KasyTabsMode mode;
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
State<KasyTabs> createState() => _KasyTabsState();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class _KasyTabsState extends State<KasyTabs> {
|
|
99
|
+
// One GlobalKey per tab to measure position/size after layout.
|
|
100
|
+
late List<GlobalKey> _keys;
|
|
101
|
+
|
|
102
|
+
// Indicator geometry (left offset, width) resolved from measured keys.
|
|
103
|
+
double _indicatorLeft = 0;
|
|
104
|
+
double _indicatorWidth = 0;
|
|
105
|
+
|
|
106
|
+
// Whether we have a valid measurement yet.
|
|
107
|
+
bool _measured = false;
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
void initState() {
|
|
111
|
+
super.initState();
|
|
112
|
+
_keys = List.generate(
|
|
113
|
+
widget.items.length,
|
|
114
|
+
(_) => GlobalKey(),
|
|
115
|
+
);
|
|
116
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
void didUpdateWidget(KasyTabs old) {
|
|
121
|
+
super.didUpdateWidget(old);
|
|
122
|
+
// Rebuild keys list if tab count changes.
|
|
123
|
+
if (old.items.length != widget.items.length) {
|
|
124
|
+
_keys = List.generate(
|
|
125
|
+
widget.items.length,
|
|
126
|
+
(_) => GlobalKey(),
|
|
127
|
+
);
|
|
128
|
+
_measured = false;
|
|
129
|
+
}
|
|
130
|
+
// Re-measure whenever selected index changes or keys were rebuilt.
|
|
131
|
+
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
void _measure() {
|
|
135
|
+
if (!mounted) return;
|
|
136
|
+
if (widget.items.isEmpty) return;
|
|
137
|
+
|
|
138
|
+
final int clampedIndex = widget.selectedIndex.clamp(
|
|
139
|
+
0,
|
|
140
|
+
widget.items.length - 1,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
final RenderBox? containerBox =
|
|
144
|
+
context.findRenderObject() as RenderBox?;
|
|
145
|
+
if (containerBox == null) return;
|
|
146
|
+
|
|
147
|
+
final GlobalKey key = _keys[clampedIndex];
|
|
148
|
+
final RenderBox? tabBox =
|
|
149
|
+
key.currentContext?.findRenderObject() as RenderBox?;
|
|
150
|
+
if (tabBox == null) return;
|
|
151
|
+
|
|
152
|
+
final Offset tabOffset = tabBox.localToGlobal(
|
|
153
|
+
Offset.zero,
|
|
154
|
+
ancestor: containerBox,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
final double newLeft = tabOffset.dx;
|
|
158
|
+
final double newWidth = tabBox.size.width;
|
|
159
|
+
|
|
160
|
+
if (!_measured ||
|
|
161
|
+
(_indicatorLeft - newLeft).abs() > 0.1 ||
|
|
162
|
+
(_indicatorWidth - newWidth).abs() > 0.1) {
|
|
163
|
+
setState(() {
|
|
164
|
+
_indicatorLeft = newLeft;
|
|
165
|
+
_indicatorWidth = newWidth;
|
|
166
|
+
_measured = true;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@override
|
|
172
|
+
Widget build(BuildContext context) {
|
|
173
|
+
if (widget.items.isEmpty) return const SizedBox.shrink();
|
|
174
|
+
|
|
175
|
+
return widget.variant == KasyTabsVariant.primary
|
|
176
|
+
? _buildPrimary(context)
|
|
177
|
+
: _buildSecondary(context);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Primary (pill indicator) ───────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
Widget _buildPrimary(BuildContext context) {
|
|
183
|
+
final KasyColors c = context.colors;
|
|
184
|
+
final bool isDark = context.isDark;
|
|
185
|
+
|
|
186
|
+
return DecoratedBox(
|
|
187
|
+
decoration: BoxDecoration(
|
|
188
|
+
color: c.avatarFallbackFill,
|
|
189
|
+
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
190
|
+
),
|
|
191
|
+
child: Padding(
|
|
192
|
+
padding: const EdgeInsets.all(4),
|
|
193
|
+
child: Stack(
|
|
194
|
+
children: [
|
|
195
|
+
// Animated pill background.
|
|
196
|
+
if (_measured)
|
|
197
|
+
AnimatedPositioned(
|
|
198
|
+
duration: const Duration(milliseconds: 250),
|
|
199
|
+
curve: Curves.easeInOut,
|
|
200
|
+
left: _indicatorLeft,
|
|
201
|
+
top: 0,
|
|
202
|
+
bottom: 0,
|
|
203
|
+
width: _indicatorWidth,
|
|
204
|
+
child: DecoratedBox(
|
|
205
|
+
decoration: BoxDecoration(
|
|
206
|
+
color: c.surface,
|
|
207
|
+
borderRadius: BorderRadius.circular(KasyRadius.full),
|
|
208
|
+
boxShadow: [
|
|
209
|
+
BoxShadow(
|
|
210
|
+
color: Colors.black.withValues(
|
|
211
|
+
alpha: isDark ? 0.18 : 0.08,
|
|
212
|
+
),
|
|
213
|
+
blurRadius: 8,
|
|
214
|
+
offset: const Offset(0, 2),
|
|
215
|
+
),
|
|
216
|
+
BoxShadow(
|
|
217
|
+
color: Colors.black.withValues(
|
|
218
|
+
alpha: isDark ? 0.10 : 0.04,
|
|
219
|
+
),
|
|
220
|
+
spreadRadius: 1,
|
|
221
|
+
),
|
|
222
|
+
],
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
// Tab labels (on top of the pill).
|
|
227
|
+
Row(
|
|
228
|
+
mainAxisSize: widget.mode == KasyTabsMode.fill
|
|
229
|
+
? MainAxisSize.max
|
|
230
|
+
: MainAxisSize.min,
|
|
231
|
+
children: List.generate(widget.items.length, (i) {
|
|
232
|
+
final KasyTabItem item = widget.items[i];
|
|
233
|
+
final bool selected = i == widget.selectedIndex;
|
|
234
|
+
return _PrimaryTab(
|
|
235
|
+
key: _keys[i],
|
|
236
|
+
item: item,
|
|
237
|
+
selected: selected,
|
|
238
|
+
expand: widget.mode == KasyTabsMode.fill,
|
|
239
|
+
onTap: item.enabled ? () => widget.onTabSelected(i) : null,
|
|
240
|
+
);
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
],
|
|
244
|
+
),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Secondary (underline indicator) ───────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
Widget _buildSecondary(BuildContext context) {
|
|
252
|
+
final KasyColors c = context.colors;
|
|
253
|
+
|
|
254
|
+
return Stack(
|
|
255
|
+
clipBehavior: Clip.none,
|
|
256
|
+
children: [
|
|
257
|
+
// Full-width bottom divider.
|
|
258
|
+
Positioned(
|
|
259
|
+
left: 0,
|
|
260
|
+
right: 0,
|
|
261
|
+
bottom: 0,
|
|
262
|
+
child: DecoratedBox(
|
|
263
|
+
decoration: BoxDecoration(
|
|
264
|
+
color: c.outline.withValues(alpha: 0.2),
|
|
265
|
+
),
|
|
266
|
+
child: const SizedBox(height: 1),
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
// Animated underline indicator.
|
|
270
|
+
if (_measured)
|
|
271
|
+
AnimatedPositioned(
|
|
272
|
+
duration: const Duration(milliseconds: 250),
|
|
273
|
+
curve: Curves.easeInOut,
|
|
274
|
+
left: _indicatorLeft,
|
|
275
|
+
bottom: 0,
|
|
276
|
+
width: _indicatorWidth,
|
|
277
|
+
height: 2,
|
|
278
|
+
child: DecoratedBox(
|
|
279
|
+
decoration: BoxDecoration(
|
|
280
|
+
color: c.primary,
|
|
281
|
+
borderRadius: BorderRadius.circular(1),
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
// Tab labels.
|
|
286
|
+
Row(
|
|
287
|
+
mainAxisSize: widget.mode == KasyTabsMode.fill
|
|
288
|
+
? MainAxisSize.max
|
|
289
|
+
: MainAxisSize.min,
|
|
290
|
+
children: List.generate(widget.items.length, (i) {
|
|
291
|
+
final KasyTabItem item = widget.items[i];
|
|
292
|
+
final bool selected = i == widget.selectedIndex;
|
|
293
|
+
return _SecondaryTab(
|
|
294
|
+
key: _keys[i],
|
|
295
|
+
item: item,
|
|
296
|
+
selected: selected,
|
|
297
|
+
expand: widget.mode == KasyTabsMode.fill,
|
|
298
|
+
onTap: item.enabled ? () => widget.onTabSelected(i) : null,
|
|
299
|
+
);
|
|
300
|
+
}),
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
308
|
+
// Internal tab widgets
|
|
309
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
class _PrimaryTab extends StatelessWidget {
|
|
312
|
+
const _PrimaryTab({
|
|
313
|
+
super.key,
|
|
314
|
+
required this.item,
|
|
315
|
+
required this.selected,
|
|
316
|
+
required this.expand,
|
|
317
|
+
required this.onTap,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
final KasyTabItem item;
|
|
321
|
+
final bool selected;
|
|
322
|
+
final bool expand;
|
|
323
|
+
final VoidCallback? onTap;
|
|
324
|
+
|
|
325
|
+
Widget _tabContent(BuildContext context) {
|
|
326
|
+
final KasyColors c = context.colors;
|
|
327
|
+
final bool disabled = !item.enabled;
|
|
328
|
+
|
|
329
|
+
return GestureDetector(
|
|
330
|
+
onTap: onTap,
|
|
331
|
+
behavior: HitTestBehavior.opaque,
|
|
332
|
+
child: Padding(
|
|
333
|
+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
334
|
+
child: Row(
|
|
335
|
+
mainAxisSize: MainAxisSize.min,
|
|
336
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
337
|
+
children: [
|
|
338
|
+
if (item.icon != null) ...[
|
|
339
|
+
Opacity(
|
|
340
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
341
|
+
child: Icon(
|
|
342
|
+
item.icon,
|
|
343
|
+
size: 16,
|
|
344
|
+
color: selected ? c.onSurface : c.muted,
|
|
345
|
+
),
|
|
346
|
+
),
|
|
347
|
+
const SizedBox(width: 6),
|
|
348
|
+
],
|
|
349
|
+
Opacity(
|
|
350
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
351
|
+
child: Text(
|
|
352
|
+
item.label,
|
|
353
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
354
|
+
color: selected ? c.onSurface : c.muted,
|
|
355
|
+
fontWeight: selected ? FontWeight.w500 : FontWeight.w400,
|
|
356
|
+
),
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
],
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@override
|
|
366
|
+
Widget build(BuildContext context) {
|
|
367
|
+
final Widget inner = _tabContent(context);
|
|
368
|
+
return expand ? Expanded(child: inner) : inner;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
class _SecondaryTab extends StatelessWidget {
|
|
373
|
+
const _SecondaryTab({
|
|
374
|
+
super.key,
|
|
375
|
+
required this.item,
|
|
376
|
+
required this.selected,
|
|
377
|
+
required this.expand,
|
|
378
|
+
required this.onTap,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
final KasyTabItem item;
|
|
382
|
+
final bool selected;
|
|
383
|
+
final bool expand;
|
|
384
|
+
final VoidCallback? onTap;
|
|
385
|
+
|
|
386
|
+
Widget _tabContent(BuildContext context) {
|
|
387
|
+
final KasyColors c = context.colors;
|
|
388
|
+
final bool disabled = !item.enabled;
|
|
389
|
+
|
|
390
|
+
return GestureDetector(
|
|
391
|
+
onTap: onTap,
|
|
392
|
+
behavior: HitTestBehavior.opaque,
|
|
393
|
+
child: Padding(
|
|
394
|
+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
395
|
+
child: Row(
|
|
396
|
+
mainAxisSize: MainAxisSize.min,
|
|
397
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
398
|
+
children: [
|
|
399
|
+
if (item.icon != null) ...[
|
|
400
|
+
Opacity(
|
|
401
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
402
|
+
child: Icon(
|
|
403
|
+
item.icon,
|
|
404
|
+
size: 16,
|
|
405
|
+
color: selected ? c.onSurface : c.muted,
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
const SizedBox(width: 6),
|
|
409
|
+
],
|
|
410
|
+
Opacity(
|
|
411
|
+
opacity: disabled ? 0.4 : 1.0,
|
|
412
|
+
child: Text(
|
|
413
|
+
item.label,
|
|
414
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
415
|
+
color: selected ? c.onSurface : c.muted,
|
|
416
|
+
fontWeight: selected ? FontWeight.w600 : FontWeight.w400,
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
],
|
|
421
|
+
),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
@override
|
|
427
|
+
Widget build(BuildContext context) {
|
|
428
|
+
final Widget inner = _tabContent(context);
|
|
429
|
+
return expand ? Expanded(child: inner) : inner;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -96,11 +96,29 @@ final class KasyPaddedSurfaceBottomBarFactory extends BartBottomBarFactory {
|
|
|
96
96
|
),
|
|
97
97
|
Theme(
|
|
98
98
|
data: Theme.of(context).copyWith(
|
|
99
|
+
splashFactory: NoSplash.splashFactory,
|
|
100
|
+
splashColor: Colors.transparent,
|
|
101
|
+
highlightColor: Colors.transparent,
|
|
99
102
|
navigationBarTheme: NavigationBarTheme.of(context).copyWith(
|
|
100
103
|
backgroundColor: Colors.transparent,
|
|
101
104
|
elevation: 0,
|
|
102
105
|
shadowColor: Colors.transparent,
|
|
103
106
|
surfaceTintColor: Colors.transparent,
|
|
107
|
+
indicatorColor: Colors.transparent,
|
|
108
|
+
iconTheme: WidgetStateProperty.resolveWith((states) {
|
|
109
|
+
final selected = states.contains(WidgetState.selected);
|
|
110
|
+
return IconThemeData(
|
|
111
|
+
color: selected ? colors.primary : colors.muted,
|
|
112
|
+
);
|
|
113
|
+
}),
|
|
114
|
+
labelTextStyle: WidgetStateProperty.resolveWith((states) {
|
|
115
|
+
final selected = states.contains(WidgetState.selected);
|
|
116
|
+
final base = Theme.of(context).textTheme.labelMedium ??
|
|
117
|
+
const TextStyle();
|
|
118
|
+
return base.copyWith(
|
|
119
|
+
color: selected ? colors.primary : colors.muted,
|
|
120
|
+
);
|
|
121
|
+
}),
|
|
104
122
|
),
|
|
105
123
|
),
|
|
106
124
|
child: BartMaterial3BottomBar(
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'dart:io' show Platform;
|
|
3
|
+
|
|
4
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
1
5
|
import 'package:home_widget/home_widget.dart';
|
|
2
6
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
3
7
|
import 'package:kasy_kit/core/home_widgets/home_widget_service.dart';
|
|
4
|
-
import 'package:kasy_kit/core/states/translations.dart';
|
|
5
8
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
6
9
|
import 'package:kasy_kit/features/subscription/repositories/subscription_repository.dart';
|
|
7
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -19,10 +22,11 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
19
22
|
@override
|
|
20
23
|
void build() {
|
|
21
24
|
// Auto-refresh the widget whenever user state changes in a way that
|
|
22
|
-
// affects what it renders (login/logout, name, premium status).
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
25
|
+
// affects what it renders (login/logout, name, email, premium status).
|
|
26
|
+
// The initial render is triggered explicitly by HomeWidgetsManager.init()
|
|
27
|
+
// after setAppGroupId completes — putting it here would race with the
|
|
28
|
+
// app-group setup and the first saveWidgetData could land in the wrong
|
|
29
|
+
// UserDefaults suite.
|
|
26
30
|
ref.listen(userStateNotifierProvider, (previous, next) {
|
|
27
31
|
if (previous == null) return;
|
|
28
32
|
if (_widgetSignature(previous.user) != _widgetSignature(next.user)) {
|
|
@@ -33,7 +37,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
33
37
|
|
|
34
38
|
/// Snapshot of the user fields the widget reads. Used to skip the update
|
|
35
39
|
/// when an unrelated field changes (e.g. lastUpdateDate refresh).
|
|
36
|
-
(String?, String?, bool) _widgetSignature(User user) {
|
|
40
|
+
(String?, String?, String?, bool) _widgetSignature(User user) {
|
|
37
41
|
final isPro = switch (user) {
|
|
38
42
|
AuthenticatedUserData(:final subscription) ||
|
|
39
43
|
AnonymousUserData(:final subscription) =>
|
|
@@ -44,34 +48,62 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
44
48
|
AuthenticatedUserData(:final name) => name,
|
|
45
49
|
_ => null,
|
|
46
50
|
};
|
|
47
|
-
|
|
51
|
+
final email = switch (user) {
|
|
52
|
+
AuthenticatedUserData(:final email) => email,
|
|
53
|
+
_ => null,
|
|
54
|
+
};
|
|
55
|
+
return (user.idOrNull, name, email, isPro);
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
@override
|
|
51
|
-
Future<void> update()
|
|
59
|
+
Future<void> update() => updateForLocale(LocaleSettings.currentLocale);
|
|
60
|
+
|
|
61
|
+
/// Same as [update] but renders against an explicit locale. Use this
|
|
62
|
+
/// from the language picker so the widget never falls one step behind:
|
|
63
|
+
/// `LocaleSettings.setLocale` propagates to `currentLocale` over a
|
|
64
|
+
/// frame boundary, and an [update] call scheduled at the same time
|
|
65
|
+
/// can race with it. Passing the locale removes the race.
|
|
66
|
+
Future<void> updateForLocale(AppLocale locale) async {
|
|
52
67
|
final logger = Logger();
|
|
53
|
-
logger.i('🔄 Updating MyWidget Home Widget');
|
|
68
|
+
logger.i('🔄 Updating MyWidget Home Widget (${locale.languageCode})');
|
|
54
69
|
final user = ref.read(userStateNotifierProvider).user;
|
|
55
|
-
final t =
|
|
70
|
+
final t = locale.translations;
|
|
71
|
+
|
|
72
|
+
// "Logged out" = no user id at all (post-logout in authRequired mode, or
|
|
73
|
+
// before any anonymous signup completes). In this state we show a
|
|
74
|
+
// come-back message and hide the plan tag — showing a plan would be
|
|
75
|
+
// misleading when there is no account behind it.
|
|
76
|
+
final isLoggedOut = user.idOrNull == null;
|
|
56
77
|
|
|
57
78
|
final name = switch (user) {
|
|
58
79
|
AuthenticatedUserData(:final name)
|
|
59
80
|
when name != null && name.isNotEmpty =>
|
|
60
81
|
name.split(' ').first,
|
|
82
|
+
// Fallback when the Firestore profile has no name yet: derive a
|
|
83
|
+
// display name from the email local-part (matches what the
|
|
84
|
+
// settings page shows).
|
|
85
|
+
AuthenticatedUserData(:final email) => email.split('@').first,
|
|
61
86
|
_ => null,
|
|
62
87
|
};
|
|
63
88
|
|
|
64
|
-
final isPro = await _resolveIsPro(user);
|
|
89
|
+
final isPro = !isLoggedOut && await _resolveIsPro(user);
|
|
65
90
|
|
|
66
91
|
final greeting = _greeting(t);
|
|
67
|
-
final title =
|
|
68
|
-
? t.home_widget.
|
|
69
|
-
:
|
|
70
|
-
|
|
92
|
+
final title = isLoggedOut
|
|
93
|
+
? t.home_widget.title_logged_out
|
|
94
|
+
: name == null
|
|
95
|
+
? t.home_widget.title_default
|
|
96
|
+
: t.home_widget.title_with_name(name: name);
|
|
97
|
+
// Empty planText is the contract used by the native widget to skip
|
|
98
|
+
// rendering the pill — see MyWidget.swift / MyWidget.kt.
|
|
99
|
+
final planText = isLoggedOut
|
|
100
|
+
? ''
|
|
101
|
+
: (isPro ? t.home_widget.plan_pro : t.home_widget.plan_free);
|
|
102
|
+
final quote = t.home_widget.quote;
|
|
71
103
|
|
|
72
104
|
logger.d(
|
|
73
105
|
'Widget payload → greeting: "$greeting", title: "$title", '
|
|
74
|
-
'planText: "$planText", isPro: $isPro',
|
|
106
|
+
'planText: "$planText", isPro: $isPro, loggedOut: $isLoggedOut',
|
|
75
107
|
);
|
|
76
108
|
|
|
77
109
|
return updateWidget({
|
|
@@ -79,6 +111,7 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
79
111
|
'title': title,
|
|
80
112
|
'planText': planText,
|
|
81
113
|
'isPro': isPro.toString(),
|
|
114
|
+
'quote': quote,
|
|
82
115
|
});
|
|
83
116
|
}
|
|
84
117
|
|
|
@@ -98,9 +131,14 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
98
131
|
if (userId == null) return cached;
|
|
99
132
|
try {
|
|
100
133
|
final repo = ref.read(subscriptionRepositoryProvider);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
134
|
+
// 2s timeout — if RevenueCat/network is slow, fall back to the cached
|
|
135
|
+
// value so the widget renders promptly on first install. The next
|
|
136
|
+
// background tick (or any user-state change) will reconcile later.
|
|
137
|
+
return await Future(() async {
|
|
138
|
+
await repo.initUser(userId);
|
|
139
|
+
final fresh = await repo.get(userId);
|
|
140
|
+
return fresh.isActive;
|
|
141
|
+
}).timeout(const Duration(seconds: 2), onTimeout: () => cached);
|
|
104
142
|
} catch (e) {
|
|
105
143
|
Logger().w('Widget could not refresh subscription: $e (using cached)');
|
|
106
144
|
return cached;
|
|
@@ -112,6 +150,22 @@ class MyWidgetHomeWidget extends _$MyWidgetHomeWidget
|
|
|
112
150
|
await HomeWidget.saveWidgetData<String>('title', data['title'] ?? '');
|
|
113
151
|
await HomeWidget.saveWidgetData<String>('planText', data['planText'] ?? '');
|
|
114
152
|
await HomeWidget.saveWidgetData<String>('isPro', data['isPro'] ?? 'false');
|
|
153
|
+
await HomeWidget.saveWidgetData<String>('quote', data['quote'] ?? '');
|
|
154
|
+
|
|
155
|
+
// On Android, saveWidgetData writes via SharedPreferences.apply() which
|
|
156
|
+
// is asynchronous. Glance's HomeWidgetGlanceStateDefinition reads from
|
|
157
|
+
// the same prefs, but a tight saveWidgetData→updateWidget sequence can
|
|
158
|
+
// race the apply() flush — Glance recomposes with the previous values
|
|
159
|
+
// (most visible right after a locale change). Fire two updates spaced
|
|
160
|
+
// out by 400ms each so even slow devices catch the new state.
|
|
161
|
+
if (!kIsWeb && Platform.isAndroid) {
|
|
162
|
+
await Future<void>.delayed(const Duration(milliseconds: 400));
|
|
163
|
+
await HomeWidget.updateWidget(
|
|
164
|
+
name: _androidWidgetName,
|
|
165
|
+
iOSName: _iosWidgetName,
|
|
166
|
+
);
|
|
167
|
+
await Future<void>.delayed(const Duration(milliseconds: 400));
|
|
168
|
+
}
|
|
115
169
|
|
|
116
170
|
await HomeWidget.updateWidget(
|
|
117
171
|
name: _androidWidgetName,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
1
3
|
import 'package:background_fetch/background_fetch.dart';
|
|
2
4
|
import 'package:flutter/foundation.dart';
|
|
3
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
@@ -8,14 +10,7 @@ import 'package:kasy_kit/core/initializer/onstart_service.dart';
|
|
|
8
10
|
import 'package:logger/logger.dart';
|
|
9
11
|
|
|
10
12
|
final homeWidgetsManagerProvider = Provider<HomeWidgetsManager>(
|
|
11
|
-
(ref)
|
|
12
|
-
// Force-build the widget service at app startup so its user-state
|
|
13
|
-
// listener attaches and the widget auto-refreshes when subscription
|
|
14
|
-
// status or other relevant fields change. Without this read, the
|
|
15
|
-
// listener would only attach on first manual update.
|
|
16
|
-
ref.read(myWidgetHomeWidgetProvider.notifier);
|
|
17
|
-
return HomeWidgetsManager();
|
|
18
|
-
},
|
|
13
|
+
(ref) => HomeWidgetsManager(ref),
|
|
19
14
|
);
|
|
20
15
|
|
|
21
16
|
const String appGroupId = 'group.com.aicrus.firebase.kit';
|
|
@@ -26,12 +21,31 @@ const String appGroupId = 'group.com.aicrus.firebase.kit';
|
|
|
26
21
|
/// will be used to initialize the home widgets and set the app group id
|
|
27
22
|
/// Register the background task for the home widgets
|
|
28
23
|
class HomeWidgetsManager implements OnStartService {
|
|
24
|
+
HomeWidgetsManager(this._ref);
|
|
25
|
+
|
|
26
|
+
final Ref _ref;
|
|
27
|
+
|
|
29
28
|
@override
|
|
30
29
|
Future<void> init() async {
|
|
31
30
|
if (kIsWeb) return;
|
|
32
31
|
try {
|
|
32
|
+
// Must be set BEFORE any saveWidgetData call, otherwise the data lands
|
|
33
|
+
// in the default UserDefaults suite and the native widget extension
|
|
34
|
+
// (which reads from the app group) sees nothing.
|
|
33
35
|
await HomeWidget.setAppGroupId(appGroupId);
|
|
34
36
|
|
|
37
|
+
// Read the widget notifier so its user-state listener attaches —
|
|
38
|
+
// future state changes (login/logout, subscription) auto-refresh
|
|
39
|
+
// the widget without waiting for the 15-min background tick.
|
|
40
|
+
final myWidget = _ref.read(myWidgetHomeWidgetProvider.notifier);
|
|
41
|
+
// Push initial data so the widget renders something on first install
|
|
42
|
+
// instead of staying blank until the background task fires.
|
|
43
|
+
// Fire-and-forget: we do NOT await here because update() may do a
|
|
44
|
+
// network call (RevenueCat) that could stall app startup and even
|
|
45
|
+
// prevent BackgroundFetch from being configured. setAppGroupId has
|
|
46
|
+
// already completed, so it is safe to fire it off now.
|
|
47
|
+
unawaited(myWidget.update());
|
|
48
|
+
|
|
35
49
|
final status = await BackgroundFetch.configure(
|
|
36
50
|
BackgroundFetchConfig(
|
|
37
51
|
minimumFetchInterval: 15,
|