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
|
@@ -4,7 +4,6 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
|
4
4
|
import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
|
|
5
5
|
import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
|
|
6
6
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
7
|
-
import 'package:kasy_kit/core/states/components/maybe_ask_biometric_setup.dart';
|
|
8
7
|
import 'package:kasy_kit/core/states/components/maybe_ask_rating.dart';
|
|
9
8
|
import 'package:kasy_kit/core/states/components/maybe_show_update_bottom_sheet.dart';
|
|
10
9
|
import 'package:kasy_kit/core/states/components/maybeshow_component.dart';
|
|
@@ -12,7 +11,6 @@ import 'package:kasy_kit/core/theme/theme.dart';
|
|
|
12
11
|
import 'package:kasy_kit/features/home/home_components_page.dart';
|
|
13
12
|
import 'package:kasy_kit/features/home/home_features_page.dart';
|
|
14
13
|
import 'package:kasy_kit/features/notifications/shared/att_permission.dart';
|
|
15
|
-
import 'package:kasy_kit/features/notifications/shared/notification_permission_bottom_sheet.dart';
|
|
16
14
|
import 'package:kasy_kit/features/subscription/shared/maybeshow_premium.dart';
|
|
17
15
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
18
16
|
|
|
@@ -27,13 +25,9 @@ class HomePage extends ConsumerWidget {
|
|
|
27
25
|
eventWidgets: [
|
|
28
26
|
MaybeAskForReview(),
|
|
29
27
|
MaybeAskForRating(),
|
|
30
|
-
// First OnAppStart handler that may return true — premium / notifications
|
|
31
|
-
// no longer suppress the biometric one-shot prompt.
|
|
32
|
-
MaybeAskBiometricSetup(),
|
|
33
28
|
MaybeShowPremiumPage(),
|
|
34
29
|
MaybeShowUpdateBottomSheet(),
|
|
35
30
|
MaybeShowAttPermission(),
|
|
36
|
-
MaybeShowNotificationPermission(),
|
|
37
31
|
],
|
|
38
32
|
child: ColoredBox(
|
|
39
33
|
color: context.colors.background,
|
|
@@ -38,6 +38,15 @@ abstract class DeviceApi {
|
|
|
38
38
|
/// Unregister the device in the backend
|
|
39
39
|
Future<void> unregister(String userId, String deviceId);
|
|
40
40
|
|
|
41
|
+
/// Heartbeat — update the `lastUpdateDate` on the current device doc.
|
|
42
|
+
/// Used so the backend can detect orphaned device docs from previous installs.
|
|
43
|
+
Future<void> touch(String userId, String installationId);
|
|
44
|
+
|
|
45
|
+
/// Delete device docs of the same user that haven't been touched in a while.
|
|
46
|
+
/// Called after registering a fresh installation to remove orphans left by
|
|
47
|
+
/// previous installs (whose installationId no longer matches).
|
|
48
|
+
Future<void> cleanupStaleDevices(String userId, String currentInstallationId);
|
|
49
|
+
|
|
41
50
|
/// Listen to token refresh
|
|
42
51
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh);
|
|
43
52
|
|
|
@@ -167,6 +176,54 @@ class FirebaseDeviceApi implements DeviceApi {
|
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
@override
|
|
180
|
+
Future<void> touch(String userId, String installationId) async {
|
|
181
|
+
try {
|
|
182
|
+
await retryOnFirestoreUnavailable(
|
|
183
|
+
() => _client
|
|
184
|
+
.collection('users')
|
|
185
|
+
.doc(userId)
|
|
186
|
+
.collection('devices')
|
|
187
|
+
.doc(installationId)
|
|
188
|
+
.update({'lastUpdateDate': Timestamp.now()}),
|
|
189
|
+
);
|
|
190
|
+
} catch (_) {
|
|
191
|
+
// Missing doc — happens if the device was unregistered or never saved.
|
|
192
|
+
// Caller (DeviceRepository) recovers by re-registering on next session.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@override
|
|
197
|
+
Future<void> cleanupStaleDevices(
|
|
198
|
+
String userId,
|
|
199
|
+
String currentInstallationId,
|
|
200
|
+
) async {
|
|
201
|
+
// Devices not touched in the last 30 days are treated as orphans from
|
|
202
|
+
// previous installations on the same physical device. Real second devices
|
|
203
|
+
// that the user actively uses stay above this threshold via heartbeat.
|
|
204
|
+
final cutoff = DateTime.now().subtract(const Duration(days: 30));
|
|
205
|
+
try {
|
|
206
|
+
final snapshot = await _client
|
|
207
|
+
.collection('users')
|
|
208
|
+
.doc(userId)
|
|
209
|
+
.collection('devices')
|
|
210
|
+
.where('lastUpdateDate', isLessThan: Timestamp.fromDate(cutoff))
|
|
211
|
+
.get();
|
|
212
|
+
final batch = _client.batch();
|
|
213
|
+
var hasDeletions = false;
|
|
214
|
+
for (final doc in snapshot.docs) {
|
|
215
|
+
if (doc.id == currentInstallationId) continue;
|
|
216
|
+
batch.delete(doc.reference);
|
|
217
|
+
hasDeletions = true;
|
|
218
|
+
}
|
|
219
|
+
if (hasDeletions) {
|
|
220
|
+
await batch.commit();
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {
|
|
223
|
+
Logger().w('cleanupStaleDevices failed: $e');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
170
227
|
@override
|
|
171
228
|
void onTokenRefresh(OnTokenRefresh onTokenRefresh) {
|
|
172
229
|
_onTokenRefreshSubscription =
|
|
@@ -117,6 +117,8 @@ sealed class NotificationPermission {
|
|
|
117
117
|
await permission.ask();
|
|
118
118
|
case NotificationPermissionDenied():
|
|
119
119
|
await permission.ask();
|
|
120
|
+
case NotificationPermissionPermanentlyDenied():
|
|
121
|
+
await permission.openSettings();
|
|
120
122
|
case NotificationPermissionGranted():
|
|
121
123
|
await permission.ensureSetup();
|
|
122
124
|
}
|
|
@@ -151,7 +153,7 @@ class NotificationPermissionGranted extends NotificationPermission {
|
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
|
-
/// we asked for permission and it was denied
|
|
156
|
+
/// we asked for permission and it was denied (but can still be asked again)
|
|
155
157
|
class NotificationPermissionDenied extends NotificationPermission {
|
|
156
158
|
final NotificationSettings? _notificationSettings;
|
|
157
159
|
final NotificationsRepository? _repository;
|
|
@@ -175,6 +177,14 @@ class NotificationPermissionDenied extends NotificationPermission {
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/// User denied the permission and the OS will not show the native prompt again.
|
|
181
|
+
/// The only way back is the system settings of the app.
|
|
182
|
+
class NotificationPermissionPermanentlyDenied extends NotificationPermission {
|
|
183
|
+
Future<void> openSettings() async {
|
|
184
|
+
await openAppSettings();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
/// we never asked for permission
|
|
179
189
|
class NotificationPermissionWaiting extends NotificationPermission {
|
|
180
190
|
final NotificationSettings? _notificationSettings;
|
|
@@ -54,6 +54,9 @@ class DeviceRepository {
|
|
|
54
54
|
// Without the userId check, a new anonymous user (same device token) would
|
|
55
55
|
// reuse the cached prefs and never register in the database.
|
|
56
56
|
if (device != null && device.token == newDevice.token && cachedUserId == userId) {
|
|
57
|
+
// Heartbeat — keeps the backend's stale-device filter (60d TTL) from
|
|
58
|
+
// dropping this active install. Cheap: one Firestore update per launch.
|
|
59
|
+
await _deviceApi.touch(userId, newDevice.installationId);
|
|
57
60
|
return;
|
|
58
61
|
}
|
|
59
62
|
// Include device properties so the welcome notification fires in the correct locale.
|
|
@@ -62,6 +65,12 @@ class DeviceRepository {
|
|
|
62
65
|
final response = await _deviceApi.register(userId, deviceWithLocale);
|
|
63
66
|
await _saveInPrefs(response);
|
|
64
67
|
await _prefs.setString(_deviceUserIdPrefsKey, userId);
|
|
68
|
+
// Remove orphan devices left by previous installs on the same physical
|
|
69
|
+
// device (different installationId, never touched again). Without this,
|
|
70
|
+
// a re-install (Xcode -> TestFlight, or TestFlight build update with an
|
|
71
|
+
// uninstall in between) leaves stale tokens that still receive push,
|
|
72
|
+
// causing duplicated notifications.
|
|
73
|
+
await _deviceApi.cleanupStaleDevices(userId, newDevice.installationId);
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
Future<void> unregister(String userId) async {
|
package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart
CHANGED
|
@@ -158,10 +158,7 @@ class AppNotificationsRepository implements NotificationsRepository {
|
|
|
158
158
|
repository: this,
|
|
159
159
|
);
|
|
160
160
|
case PermissionStatus.permanentlyDenied:
|
|
161
|
-
return
|
|
162
|
-
notificationSettings: _notificationSettings,
|
|
163
|
-
repository: this,
|
|
164
|
-
);
|
|
161
|
+
return NotificationPermissionPermanentlyDenied();
|
|
165
162
|
default:
|
|
166
163
|
return NotificationPermissionWaiting(
|
|
167
164
|
notificationSettings: _notificationSettings,
|
|
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
4
4
|
import 'package:kasy_kit/components/components.dart';
|
|
5
5
|
import 'package:kasy_kit/core/data/api/tracking_api.dart';
|
|
6
6
|
import 'package:kasy_kit/core/icons/kasy_icons.dart';
|
|
7
|
+
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
7
8
|
import 'package:kasy_kit/core/states/components/maybeshow_component.dart';
|
|
8
9
|
import 'package:kasy_kit/core/states/models/event_model.dart';
|
|
9
10
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
@@ -13,15 +14,19 @@ import 'package:permission_handler/permission_handler.dart';
|
|
|
13
14
|
|
|
14
15
|
/// Requests App Tracking Transparency (ATT) on iOS if not yet determined.
|
|
15
16
|
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
20
|
-
///
|
|
17
|
+
/// Apple only allows the native ATT prompt to be shown once per install, so we
|
|
18
|
+
/// gate the system call behind a soft explanation dialog (KasyDialog) first.
|
|
19
|
+
/// If the user dismisses the soft dialog, we back off and ask again later (up
|
|
20
|
+
/// to [_maxSoftAttempts] times, with [_cooldown] between attempts). After the
|
|
21
|
+
/// last attempt we stop asking forever.
|
|
21
22
|
///
|
|
22
23
|
/// To remove: delete this file and remove [MaybeShowAttPermission] from
|
|
23
24
|
/// [HomePage]'s event list.
|
|
24
25
|
class MaybeShowAttPermission implements MaybeShowWithRef {
|
|
26
|
+
static const Duration _settleDelay = Duration(milliseconds: 1500);
|
|
27
|
+
static const int _maxSoftAttempts = 3;
|
|
28
|
+
static const Duration _cooldown = Duration(days: 7);
|
|
29
|
+
|
|
25
30
|
@override
|
|
26
31
|
Future<bool> handle(WidgetRef ref, AppEvent event) async {
|
|
27
32
|
if (kIsWeb) return false;
|
|
@@ -32,12 +37,28 @@ class MaybeShowAttPermission implements MaybeShowWithRef {
|
|
|
32
37
|
// `denied` in permission_handler maps to iOS "notDetermined" — never asked.
|
|
33
38
|
if (!status.isDenied) return false;
|
|
34
39
|
|
|
40
|
+
final prefs = ref.read(sharedPreferencesProvider);
|
|
41
|
+
final attempts = prefs.getAttSoftDismissCount();
|
|
42
|
+
if (attempts >= _maxSoftAttempts) return false;
|
|
43
|
+
|
|
44
|
+
final lastAsked = prefs.getAttSoftLastAskedAt();
|
|
45
|
+
if (lastAsked != null &&
|
|
46
|
+
DateTime.now().difference(lastAsked) < _cooldown) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await Future<void>.delayed(_settleDelay);
|
|
51
|
+
await prefs.setAttSoftLastAskedAt(DateTime.now());
|
|
35
52
|
if (!ref.context.mounted) return false;
|
|
36
53
|
|
|
37
54
|
final confirmed = await _showExplanationDialog(ref.context);
|
|
38
|
-
if (!confirmed)
|
|
55
|
+
if (!confirmed) {
|
|
56
|
+
await prefs.setAttSoftDismissCount(attempts + 1);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
await Permission.appTrackingTransparency.request();
|
|
61
|
+
await prefs.setAttSoftDismissCount(_maxSoftAttempts);
|
|
41
62
|
|
|
42
63
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
43
64
|
if (userId != null) {
|
|
@@ -45,8 +66,7 @@ class MaybeShowAttPermission implements MaybeShowWithRef {
|
|
|
45
66
|
ref.read(facebookEventApiProvider).initUser(userId).ignore();
|
|
46
67
|
}
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
return false;
|
|
69
|
+
return true;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
Future<bool> _showExplanationDialog(BuildContext context) async {
|
|
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|
|
4
4
|
import 'package:flutter/rendering.dart';
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:kasy_kit/components/components.dart';
|
|
7
|
+
import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
|
|
7
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
8
9
|
import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
|
|
9
10
|
as app;
|
|
@@ -20,25 +21,39 @@ class NotificationsPage extends ConsumerStatefulWidget {
|
|
|
20
21
|
ConsumerState<NotificationsPage> createState() => _NotificationsPageState();
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
class _NotificationsPageState extends ConsumerState<NotificationsPage>
|
|
24
|
+
class _NotificationsPageState extends ConsumerState<NotificationsPage>
|
|
25
|
+
with WidgetsBindingObserver {
|
|
24
26
|
final ScrollController _scrollController = ScrollController();
|
|
25
27
|
Timer? _autoReadTimer;
|
|
26
28
|
|
|
27
29
|
@override
|
|
28
30
|
void initState() {
|
|
29
31
|
super.initState();
|
|
32
|
+
WidgetsBinding.instance.addObserver(this);
|
|
30
33
|
_scrollController.addListener(_onScrollChange);
|
|
31
34
|
requestReadAll();
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
@override
|
|
35
38
|
void dispose() {
|
|
39
|
+
WidgetsBinding.instance.removeObserver(this);
|
|
36
40
|
_autoReadTimer?.cancel();
|
|
37
41
|
_scrollController.removeListener(_onScrollChange);
|
|
38
42
|
_scrollController.dispose();
|
|
39
43
|
super.dispose();
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
@override
|
|
47
|
+
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
48
|
+
super.didChangeAppLifecycleState(state);
|
|
49
|
+
if (state == AppLifecycleState.resumed && mounted) {
|
|
50
|
+
// Native permission dialogs push the app to the background briefly. When
|
|
51
|
+
// we come back, the Bart bottom bar can stay hidden because its internal
|
|
52
|
+
// visibility state is reset. Force it back so the user is never stranded.
|
|
53
|
+
kasyShowBottomBar(context);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
void _onScrollChange() {
|
|
43
58
|
final direction = _scrollController.position.userScrollDirection;
|
|
44
59
|
final isScrollingDown = direction == ScrollDirection.reverse;
|
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
3
|
import 'package:kasy_kit/components/components.dart';
|
|
4
|
+
import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
|
|
4
5
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
5
6
|
import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
|
|
6
7
|
import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
|
|
7
8
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
8
9
|
|
|
9
|
-
class EmptyNotifications extends
|
|
10
|
+
class EmptyNotifications extends ConsumerStatefulWidget {
|
|
10
11
|
const EmptyNotifications({super.key});
|
|
11
12
|
|
|
12
13
|
@override
|
|
13
|
-
|
|
14
|
+
ConsumerState<EmptyNotifications> createState() => _EmptyNotificationsState();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class _EmptyNotificationsState extends ConsumerState<EmptyNotifications> {
|
|
18
|
+
late Future<NotificationPermission> _permissionFuture;
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
void initState() {
|
|
22
|
+
super.initState();
|
|
23
|
+
_permissionFuture = _loadPermission();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Future<NotificationPermission> _loadPermission() {
|
|
27
|
+
return ref.read(notificationRepositoryProvider).getPermissionStatus();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Future<void> _onAskPressed(NotificationPermission permission) async {
|
|
31
|
+
await permission.maybeAsk();
|
|
32
|
+
if (!mounted) return;
|
|
33
|
+
// The native permission dialog pulls the app out of focus; restore the
|
|
34
|
+
// bottom bar defensively before refreshing the local state.
|
|
35
|
+
kasyShowBottomBar(context);
|
|
36
|
+
setState(() {
|
|
37
|
+
_permissionFuture = _loadPermission();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
Widget build(BuildContext context) {
|
|
14
43
|
final tr = context.t.notifications;
|
|
15
44
|
return Padding(
|
|
16
45
|
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
@@ -50,18 +79,22 @@ class EmptyNotifications extends ConsumerWidget {
|
|
|
50
79
|
),
|
|
51
80
|
const SizedBox(height: KasySpacing.xl),
|
|
52
81
|
FutureBuilder<NotificationPermission>(
|
|
53
|
-
future:
|
|
54
|
-
.read(notificationRepositoryProvider)
|
|
55
|
-
.getPermissionStatus(),
|
|
82
|
+
future: _permissionFuture,
|
|
56
83
|
builder: (context, snapshot) {
|
|
57
|
-
final
|
|
58
|
-
if (
|
|
84
|
+
final permission = snapshot.data;
|
|
85
|
+
if (permission == null) return const SizedBox.shrink();
|
|
86
|
+
if (permission is NotificationPermissionGranted) {
|
|
87
|
+
return const SizedBox.shrink();
|
|
88
|
+
}
|
|
89
|
+
final isLocked =
|
|
90
|
+
permission is NotificationPermissionPermanentlyDenied;
|
|
59
91
|
return KasyButton(
|
|
60
|
-
label: tr.empty_cta,
|
|
92
|
+
label: isLocked ? tr.empty_cta_open_settings : tr.empty_cta,
|
|
61
93
|
variant: KasyButtonVariant.soft,
|
|
62
|
-
icon:
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
icon: isLocked
|
|
95
|
+
? KasyIcons.settings
|
|
96
|
+
: KasyIcons.notificationAdd,
|
|
97
|
+
onPressed: () => _onAskPressed(permission),
|
|
65
98
|
);
|
|
66
99
|
},
|
|
67
100
|
),
|
package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart
CHANGED
|
@@ -11,7 +11,7 @@ import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
|
11
11
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
12
12
|
import 'package:kasy_kit/core/web_device_preview/web_device_preview.dart';
|
|
13
13
|
import 'package:kasy_kit/core/widgets/update_bottom_sheet.dart';
|
|
14
|
-
import 'package:kasy_kit/features/notifications/
|
|
14
|
+
import 'package:kasy_kit/features/notifications/api/local_notifier.dart';
|
|
15
15
|
import 'package:kasy_kit/features/settings/settings_page.dart';
|
|
16
16
|
import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
|
|
17
17
|
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
@@ -89,30 +89,33 @@ class _AdminSheet extends ConsumerWidget {
|
|
|
89
89
|
);
|
|
90
90
|
},
|
|
91
91
|
),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
92
|
+
if (kIsWeb) ...[
|
|
93
|
+
const SettingsDivider(),
|
|
94
|
+
ValueListenableBuilder<bool>(
|
|
95
|
+
valueListenable: webDevicePreviewEnabledNotifier,
|
|
96
|
+
builder: (context, enabled, _) {
|
|
97
|
+
return SettingsSwitchTile(
|
|
98
|
+
icon: KasyIcons.phoneAndroid,
|
|
99
|
+
title: t.settings.admin.device_preview_title,
|
|
100
|
+
value: enabled,
|
|
101
|
+
onChanged: (v) async {
|
|
102
|
+
final navigator = Navigator.of(
|
|
103
|
+
context,
|
|
104
|
+
rootNavigator: true,
|
|
105
|
+
);
|
|
106
|
+
final p =
|
|
107
|
+
await SharedPreferences.getInstance();
|
|
108
|
+
await p.setBool(
|
|
109
|
+
webDevicePreviewEnabledPrefKey,
|
|
110
|
+
v,
|
|
111
|
+
);
|
|
112
|
+
webDevicePreviewEnabledNotifier.value = v;
|
|
113
|
+
if (v) navigator.pop();
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
],
|
|
116
119
|
const SettingsDivider(),
|
|
117
120
|
SettingsTile(
|
|
118
121
|
icon: KasyIcons.note,
|
|
@@ -186,10 +189,9 @@ class _AdminSheet extends ConsumerWidget {
|
|
|
186
189
|
title: t.settings.admin.ask_notification,
|
|
187
190
|
onTap: () {
|
|
188
191
|
Navigator.pop(context);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
192
|
+
ref
|
|
193
|
+
.read(notificationsSettingsProvider)
|
|
194
|
+
.askPermission();
|
|
193
195
|
},
|
|
194
196
|
),
|
|
195
197
|
const SettingsDivider(),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
|
|
1
3
|
import 'package:flutter/material.dart';
|
|
2
4
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
5
|
+
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
3
6
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
4
7
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
5
8
|
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
@@ -93,12 +96,25 @@ class LanguageSwitcher extends ConsumerWidget {
|
|
|
93
96
|
return _LocaleOptionTile(
|
|
94
97
|
locale: locale,
|
|
95
98
|
isSelected: isSelected,
|
|
96
|
-
onTap: ()
|
|
99
|
+
onTap: () {
|
|
100
|
+
// Close the sheet FIRST. Awaiting work before pop
|
|
101
|
+
// races with the rebuild triggered by setLocale and
|
|
102
|
+
// crashed Navigator.pop with !_debugLocked.
|
|
103
|
+
Navigator.pop(sheetContext);
|
|
97
104
|
LocaleSettings.setLocale(locale);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
// Re-render the home widget in the new language —
|
|
106
|
+
// fire-and-forget so the UI doesn't wait on a
|
|
107
|
+
// network call (RevenueCat) inside update().
|
|
108
|
+
unawaited(
|
|
109
|
+
ref
|
|
110
|
+
.read(sharedPreferencesProvider)
|
|
111
|
+
.setAppLocale(locale.languageCode),
|
|
112
|
+
);
|
|
113
|
+
unawaited(
|
|
114
|
+
ref
|
|
115
|
+
.read(myWidgetHomeWidgetProvider.notifier)
|
|
116
|
+
.updateForLocale(locale),
|
|
117
|
+
);
|
|
102
118
|
},
|
|
103
119
|
);
|
|
104
120
|
}),
|
|
@@ -443,6 +443,7 @@
|
|
|
443
443
|
"group_yesterday": "Yesterday",
|
|
444
444
|
"group_older": "Older",
|
|
445
445
|
"empty_cta": "Enable notifications",
|
|
446
|
+
"empty_cta_open_settings": "Open settings",
|
|
446
447
|
"delete_all": "Delete all",
|
|
447
448
|
"delete_all_confirm_title": "Delete all notifications?",
|
|
448
449
|
"delete_all_confirm_message": "This will permanently remove every notification from your account. This cannot be undone.",
|
|
@@ -527,8 +528,10 @@
|
|
|
527
528
|
"greeting_evening": "Good evening",
|
|
528
529
|
"title_with_name": "Hi, $name!",
|
|
529
530
|
"title_default": "Hi there!",
|
|
531
|
+
"title_logged_out": "We'll be here when you're back",
|
|
530
532
|
"plan_free": "Free plan",
|
|
531
|
-
"plan_pro": "PRO"
|
|
533
|
+
"plan_pro": "PRO",
|
|
534
|
+
"quote": "It always seems impossible until it's done."
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
537
|
|
|
@@ -443,6 +443,7 @@
|
|
|
443
443
|
"group_yesterday": "Ayer",
|
|
444
444
|
"group_older": "Más antiguas",
|
|
445
445
|
"empty_cta": "Activar notificaciones",
|
|
446
|
+
"empty_cta_open_settings": "Abrir ajustes",
|
|
446
447
|
"delete_all": "Eliminar todo",
|
|
447
448
|
"delete_all_confirm_title": "¿Eliminar todas las notificaciones?",
|
|
448
449
|
"delete_all_confirm_message": "Esto eliminará todas las notificaciones de tu cuenta. Esta acción no se puede deshacer.",
|
|
@@ -527,8 +528,10 @@
|
|
|
527
528
|
"greeting_evening": "Buenas noches",
|
|
528
529
|
"title_with_name": "¡Hola, $name!",
|
|
529
530
|
"title_default": "¡Hola!",
|
|
531
|
+
"title_logged_out": "Te esperamos de vuelta",
|
|
530
532
|
"plan_free": "Plan gratuito",
|
|
531
|
-
"plan_pro": "PRO"
|
|
533
|
+
"plan_pro": "PRO",
|
|
534
|
+
"quote": "Siempre parece imposible hasta que se hace."
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
537
|
|
|
@@ -443,6 +443,7 @@
|
|
|
443
443
|
"group_yesterday": "Ontem",
|
|
444
444
|
"group_older": "Mais antigas",
|
|
445
445
|
"empty_cta": "Ativar notificações",
|
|
446
|
+
"empty_cta_open_settings": "Abrir configurações",
|
|
446
447
|
"delete_all": "Excluir tudo",
|
|
447
448
|
"delete_all_confirm_title": "Excluir todas as notificações?",
|
|
448
449
|
"delete_all_confirm_message": "Isso vai remover todas as notificações da sua conta. Essa ação não pode ser desfeita.",
|
|
@@ -527,8 +528,10 @@
|
|
|
527
528
|
"greeting_evening": "Boa noite",
|
|
528
529
|
"title_with_name": "Olá, $name!",
|
|
529
530
|
"title_default": "Olá!",
|
|
531
|
+
"title_logged_out": "Aguardamos seu retorno",
|
|
530
532
|
"plan_free": "Plano grátis",
|
|
531
|
-
"plan_pro": "PRO"
|
|
533
|
+
"plan_pro": "PRO",
|
|
534
|
+
"quote": "Sempre parece impossível, até que seja feito."
|
|
532
535
|
}
|
|
533
536
|
}
|
|
534
537
|
|
|
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
|
16
16
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
17
17
|
# In Windows, build-name is used as the major, minor, and patch parts
|
|
18
18
|
# of the product and file versions while build-number is used as the build suffix.
|
|
19
|
-
version: 1.0.0+
|
|
19
|
+
version: 1.0.0+31
|
|
20
20
|
|
|
21
21
|
environment:
|
|
22
22
|
sdk: ^3.11.0
|
|
@@ -130,6 +130,13 @@ flutter_launcher_icons:
|
|
|
130
130
|
android: ic_launcher
|
|
131
131
|
ios: true
|
|
132
132
|
remove_alpha_ios: true
|
|
133
|
+
adaptive_icon_background: assets/images/icon_android.png
|
|
134
|
+
adaptive_icon_foreground: assets/images/icon_foreground_empty.png
|
|
135
|
+
web:
|
|
136
|
+
generate: true
|
|
137
|
+
image_path: assets/images/favicon.png
|
|
138
|
+
background_color: "#01171f"
|
|
139
|
+
theme_color: "#01171f"
|
|
133
140
|
flutter_native_splash:
|
|
134
141
|
color: "#FFFFFF"
|
|
135
142
|
color_dark: "#000000"
|
|
@@ -141,8 +148,8 @@ flutter_native_splash:
|
|
|
141
148
|
android_12:
|
|
142
149
|
color: "#FFFFFF"
|
|
143
150
|
color_dark: "#000000"
|
|
144
|
-
image: assets/images/
|
|
145
|
-
image_dark: assets/images/
|
|
151
|
+
image: assets/images/splash_logo_light_android12.png
|
|
152
|
+
image_dark: assets/images/splash_logo_dark_android12.png
|
|
146
153
|
|
|
147
154
|
# To add assets to your application, add an assets section, like this:
|
|
148
155
|
# assets:
|
|
@@ -15,6 +15,15 @@ class FakeDeviceApi implements DeviceApi {
|
|
|
15
15
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
@override
|
|
19
|
+
Future<void> touch(String userId, String installationId) async {}
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
Future<void> cleanupStaleDevices(
|
|
23
|
+
String userId,
|
|
24
|
+
String currentInstallationId,
|
|
25
|
+
) async {}
|
|
26
|
+
|
|
18
27
|
@override
|
|
19
28
|
Future<DeviceEntity> get() {
|
|
20
29
|
return Future.value(
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
35
41
|
<style id="splash-screen-style">
|
|
36
42
|
html {
|
|
37
43
|
height: 100%
|
|
@@ -111,6 +117,9 @@
|
|
|
111
117
|
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
|
112
118
|
</picture>
|
|
113
119
|
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
114
123
|
<script src="flutter_bootstrap.js" async=""></script>
|
|
115
124
|
<script src="./local_notifications.js"></script>
|
|
116
125
|
|