kasy-cli 1.20.0 → 1.21.0-beta.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/README.md +11 -3
- package/lib/commands/docs.js +0 -10
- package/lib/commands/ios.js +3 -2
- package/lib/commands/new.js +98 -58
- package/lib/commands/run.js +7 -0
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +10 -8
- package/lib/scaffold/backends/supabase/deploy.js +56 -3
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/catalog.js +2 -2
- package/lib/scaffold/generate.js +19 -3
- package/lib/scaffold/shared/generator-utils.js +265 -55
- package/lib/scaffold/shared/post-build.js +22 -6
- package/lib/utils/apple-release.js +1 -10
- package/lib/utils/browser.js +61 -0
- package/lib/utils/checks.js +189 -69
- package/lib/utils/env-tools.js +101 -0
- package/lib/utils/i18n/messages-en.js +13 -1
- package/lib/utils/i18n/messages-es.js +13 -1
- package/lib/utils/i18n/messages-pt.js +13 -1
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_sidebar_pro.dart +8 -14
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +38 -128
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -125
- package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +9 -1
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -14
- package/templates/firebase/lib/features/home/home_page.dart +7 -8
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/router.dart +60 -0
- package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
- package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
- package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
- package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
- package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
|
@@ -15,6 +15,11 @@ import 'package:kasy_kit/features/authentication/ui/recover_password_page.dart';
|
|
|
15
15
|
import 'package:kasy_kit/features/authentication/ui/signin_page.dart';
|
|
16
16
|
import 'package:kasy_kit/features/authentication/ui/signup_page.dart';
|
|
17
17
|
import 'package:kasy_kit/features/feedbacks/ui/feedback_page.dart';
|
|
18
|
+
import 'package:kasy_kit/features/home/design_system_page.dart';
|
|
19
|
+
import 'package:kasy_kit/features/home/home_components_page.dart';
|
|
20
|
+
import 'package:kasy_kit/features/home/home_components_preview_page.dart';
|
|
21
|
+
import 'package:kasy_kit/features/home/home_components_preview_registry.dart';
|
|
22
|
+
import 'package:kasy_kit/features/home/home_features_page.dart';
|
|
18
23
|
import 'package:kasy_kit/features/llm_chat/llm_chat_page.dart';
|
|
19
24
|
import 'package:kasy_kit/features/local_reminder/ui/reminder_page.dart';
|
|
20
25
|
import 'package:kasy_kit/features/onboarding/ui/onboarding_page.dart';
|
|
@@ -73,6 +78,61 @@ GoRouter generateRouter({
|
|
|
73
78
|
),
|
|
74
79
|
),
|
|
75
80
|
),
|
|
81
|
+
// Home showcase detail screens. These are TOP-LEVEL routes (siblings of
|
|
82
|
+
// '/', not children of the BottomMenu shell), so go_router renders them on
|
|
83
|
+
// the root navigator: full-screen, above the bottom bar, URL-addressable.
|
|
84
|
+
// Returning pops back to the tab with its menu intact.
|
|
85
|
+
GoRoute(
|
|
86
|
+
name: 'features',
|
|
87
|
+
path: '/features',
|
|
88
|
+
pageBuilder: (context, state) => kasyTransitionPage(
|
|
89
|
+
key: state.pageKey,
|
|
90
|
+
child: const HomeFeaturesPage(),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
GoRoute(
|
|
94
|
+
name: 'design_system',
|
|
95
|
+
path: '/design-system',
|
|
96
|
+
pageBuilder: (context, state) => kasyTransitionPage(
|
|
97
|
+
key: state.pageKey,
|
|
98
|
+
child: const DesignSystemPage(),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
GoRoute(
|
|
102
|
+
name: 'components',
|
|
103
|
+
path: '/components',
|
|
104
|
+
pageBuilder: (context, state) => kasyTransitionPage(
|
|
105
|
+
key: state.pageKey,
|
|
106
|
+
child: const HomeComponentsPage(),
|
|
107
|
+
),
|
|
108
|
+
routes: [
|
|
109
|
+
// /components/:name — a single component's preview, looked up from the
|
|
110
|
+
// registry so the URL alone restores the screen on web reload.
|
|
111
|
+
GoRoute(
|
|
112
|
+
name: 'component_preview',
|
|
113
|
+
path: ':name',
|
|
114
|
+
pageBuilder: (context, state) {
|
|
115
|
+
final ComponentPreviewDefinition? definition =
|
|
116
|
+
getComponentPreviewDefinition(
|
|
117
|
+
state.pathParameters['name'] ?? '',
|
|
118
|
+
);
|
|
119
|
+
if (definition == null) {
|
|
120
|
+
return kasyTransitionPage(
|
|
121
|
+
key: state.pageKey,
|
|
122
|
+
child: const PageNotFound(),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return kasyTransitionPage(
|
|
126
|
+
key: state.pageKey,
|
|
127
|
+
child: HomeComponentsPreviewPage(
|
|
128
|
+
title: definition.title,
|
|
129
|
+
variants: definition.variants,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
),
|
|
134
|
+
],
|
|
135
|
+
),
|
|
76
136
|
GoRoute(
|
|
77
137
|
name: 'onboarding',
|
|
78
138
|
path: '/onboarding',
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
3
|
+
import 'package:go_router/go_router.dart';
|
|
4
|
+
|
|
5
|
+
/// Regression test for the Features/Components navigation.
|
|
6
|
+
///
|
|
7
|
+
/// These detail screens are TOP-LEVEL go_router routes (siblings of the home
|
|
8
|
+
/// shell, not children of it), so pushing them renders full-screen on the root
|
|
9
|
+
/// navigator: the bottom bar is gone while the detail is open, and it returns
|
|
10
|
+
/// intact when the user pops back to the tab.
|
|
11
|
+
void main() {
|
|
12
|
+
testWidgets('a top-level route opens full-screen over the bottom bar, '
|
|
13
|
+
'and the bar returns on pop', (tester) async {
|
|
14
|
+
final router = GoRouter(
|
|
15
|
+
routes: [
|
|
16
|
+
// Home shell — owns the bottom bar (like BottomMenu).
|
|
17
|
+
GoRoute(
|
|
18
|
+
path: '/',
|
|
19
|
+
builder: (context, state) => Scaffold(
|
|
20
|
+
bottomNavigationBar: const Text('MENU'),
|
|
21
|
+
body: Center(
|
|
22
|
+
child: ElevatedButton(
|
|
23
|
+
onPressed: () => context.push('/features'),
|
|
24
|
+
child: const Text('open'),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
// Detail screen — sibling of '/', so it renders on the root navigator.
|
|
30
|
+
GoRoute(
|
|
31
|
+
path: '/features',
|
|
32
|
+
builder: (context, state) => const Scaffold(
|
|
33
|
+
body: Center(child: Text('DETAIL')),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
|
40
|
+
|
|
41
|
+
// Home tab: bottom bar visible, no detail.
|
|
42
|
+
expect(find.text('MENU'), findsOneWidget);
|
|
43
|
+
expect(find.text('DETAIL'), findsNothing);
|
|
44
|
+
|
|
45
|
+
// Open Features.
|
|
46
|
+
await tester.tap(find.text('open'));
|
|
47
|
+
await tester.pumpAndSettle();
|
|
48
|
+
expect(find.text('DETAIL'), findsOneWidget);
|
|
49
|
+
expect(find.text('MENU'), findsNothing); // covered — no bottom bar here
|
|
50
|
+
|
|
51
|
+
// Back: the bottom bar is intact.
|
|
52
|
+
Navigator.of(tester.element(find.text('DETAIL'))).maybePop();
|
|
53
|
+
await tester.pumpAndSettle();
|
|
54
|
+
expect(find.text('DETAIL'), findsNothing);
|
|
55
|
+
expect(find.text('MENU'), findsOneWidget);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter_animate/flutter_animate.dart';
|
|
3
|
-
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
|
-
import 'package:go_router/go_router.dart';
|
|
5
|
-
import 'package:kasy_kit/components/components.dart';
|
|
6
|
-
import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
7
|
-
import 'package:kasy_kit/core/rating/providers/rating_repository.dart';
|
|
8
|
-
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
9
|
-
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
|
-
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
11
|
-
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
|
-
import 'package:logger/logger.dart';
|
|
13
|
-
|
|
14
|
-
Future<bool> showReviewDialog(WidgetRef ref, {bool force = false}) {
|
|
15
|
-
if (!ref.context.mounted) {
|
|
16
|
-
return Future.value(false);
|
|
17
|
-
}
|
|
18
|
-
final ratingRepository = ref.watch(ratingRepositoryProvider);
|
|
19
|
-
final userState = ref.watch(userStateNotifierProvider);
|
|
20
|
-
final ratingFuture = ratingRepository.getReview(userState.user);
|
|
21
|
-
|
|
22
|
-
return ratingFuture.then((rating) {
|
|
23
|
-
final shouldAsk = rating.shouldAsk();
|
|
24
|
-
Logger().d('should Ask for review: $shouldAsk');
|
|
25
|
-
if (!shouldAsk && !force) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
if (!ref.context.mounted) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
showDialog(
|
|
32
|
-
context: ref.context,
|
|
33
|
-
barrierDismissible: false,
|
|
34
|
-
builder: (context) {
|
|
35
|
-
ratingRepository.delay();
|
|
36
|
-
return Animate(
|
|
37
|
-
effects: const [
|
|
38
|
-
FadeEffect(
|
|
39
|
-
delay: Duration(milliseconds: 100),
|
|
40
|
-
duration: Duration(milliseconds: 300),
|
|
41
|
-
),
|
|
42
|
-
MoveEffect(
|
|
43
|
-
delay: Duration(milliseconds: 100),
|
|
44
|
-
duration: Duration(milliseconds: 450),
|
|
45
|
-
curve: Curves.easeOut,
|
|
46
|
-
begin: Offset(0, 50),
|
|
47
|
-
end: Offset.zero,
|
|
48
|
-
),
|
|
49
|
-
],
|
|
50
|
-
child: Dialog(
|
|
51
|
-
backgroundColor: Colors.transparent,
|
|
52
|
-
child: DeviceSizeBuilder(
|
|
53
|
-
builder: (device) {
|
|
54
|
-
final maxWidth = switch (device) {
|
|
55
|
-
DeviceType.medium => MediaQuery.of(context).size.width - KasySpacing.xl,
|
|
56
|
-
_ => 550.0,
|
|
57
|
-
};
|
|
58
|
-
return ConstrainedBox(
|
|
59
|
-
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
60
|
-
child: Center(
|
|
61
|
-
child: Container(
|
|
62
|
-
decoration: BoxDecoration(
|
|
63
|
-
color: context.colors.background,
|
|
64
|
-
borderRadius: KasyRadius.lgBorderRadius,
|
|
65
|
-
border: Border.all(
|
|
66
|
-
color: context.colors.onBackground.withValues(
|
|
67
|
-
alpha: .3,
|
|
68
|
-
),
|
|
69
|
-
width: 2,
|
|
70
|
-
strokeAlign: BorderSide.strokeAlignOutside,
|
|
71
|
-
),
|
|
72
|
-
),
|
|
73
|
-
child: Column(
|
|
74
|
-
mainAxisSize: MainAxisSize.min,
|
|
75
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
76
|
-
children: [
|
|
77
|
-
Flexible(
|
|
78
|
-
child: Stack(
|
|
79
|
-
children: [
|
|
80
|
-
ClipRRect(
|
|
81
|
-
borderRadius: const BorderRadius.only(
|
|
82
|
-
topLeft: Radius.circular(KasyRadius.lg),
|
|
83
|
-
topRight: Radius.circular(KasyRadius.lg),
|
|
84
|
-
),
|
|
85
|
-
child: Image.asset(
|
|
86
|
-
'assets/images/review.png',
|
|
87
|
-
fit: BoxFit.fitWidth,
|
|
88
|
-
width: maxWidth,
|
|
89
|
-
),
|
|
90
|
-
),
|
|
91
|
-
Positioned(
|
|
92
|
-
top: KasySpacing.md,
|
|
93
|
-
left: KasySpacing.md,
|
|
94
|
-
child: CloseIcon(
|
|
95
|
-
onExit: () {
|
|
96
|
-
ref
|
|
97
|
-
.read(analyticsApiProvider)
|
|
98
|
-
.logEvent('rating_popup_close', {});
|
|
99
|
-
rating.delay().then((_) {
|
|
100
|
-
if (!context.mounted) return;
|
|
101
|
-
Navigator.of(context).pop();
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
),
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
),
|
|
108
|
-
),
|
|
109
|
-
const SizedBox(height: KasySpacing.md),
|
|
110
|
-
Flexible(
|
|
111
|
-
flex: 0,
|
|
112
|
-
child: Padding(
|
|
113
|
-
padding: const EdgeInsets.symmetric(
|
|
114
|
-
horizontal: KasySpacing.smd,
|
|
115
|
-
),
|
|
116
|
-
child: Text(
|
|
117
|
-
Translations.of(context).review_popup.title,
|
|
118
|
-
textAlign: TextAlign.center,
|
|
119
|
-
style: context.textTheme.headlineSmall,
|
|
120
|
-
),
|
|
121
|
-
),
|
|
122
|
-
),
|
|
123
|
-
const SizedBox(height: KasySpacing.md),
|
|
124
|
-
Padding(
|
|
125
|
-
padding: const EdgeInsets.symmetric(
|
|
126
|
-
horizontal: KasySpacing.lg,
|
|
127
|
-
),
|
|
128
|
-
child: Text(
|
|
129
|
-
Translations.of(context).review_popup.description,
|
|
130
|
-
textAlign: TextAlign.center,
|
|
131
|
-
style: context.textTheme.bodyMedium,
|
|
132
|
-
),
|
|
133
|
-
),
|
|
134
|
-
const SizedBox(height: KasySpacing.lg),
|
|
135
|
-
Padding(
|
|
136
|
-
padding: const EdgeInsets.symmetric(
|
|
137
|
-
horizontal: KasySpacing.md,
|
|
138
|
-
),
|
|
139
|
-
child: KasyButton(
|
|
140
|
-
label: Translations.of(context).review_popup.rate_button,
|
|
141
|
-
expand: true,
|
|
142
|
-
onPressed: () {
|
|
143
|
-
ref.read(analyticsApiProvider).logEvent('rating_popup_show', {});
|
|
144
|
-
ratingRepository
|
|
145
|
-
.rate()
|
|
146
|
-
.then((res) => rating.review())
|
|
147
|
-
.then((_) {
|
|
148
|
-
if (!context.mounted) return;
|
|
149
|
-
Navigator.of(context).pop();
|
|
150
|
-
});
|
|
151
|
-
},
|
|
152
|
-
),
|
|
153
|
-
),
|
|
154
|
-
const SizedBox(height: KasySpacing.sm),
|
|
155
|
-
KasyButton(
|
|
156
|
-
label: Translations.of(context).review_popup.cancel_button,
|
|
157
|
-
variant: KasyButtonVariant.tertiary,
|
|
158
|
-
expand: true,
|
|
159
|
-
onPressed: () => Navigator.of(context).pop(true),
|
|
160
|
-
),
|
|
161
|
-
const SizedBox(height: KasySpacing.sm),
|
|
162
|
-
],
|
|
163
|
-
),
|
|
164
|
-
),
|
|
165
|
-
),
|
|
166
|
-
);
|
|
167
|
-
},
|
|
168
|
-
),
|
|
169
|
-
),
|
|
170
|
-
);
|
|
171
|
-
},
|
|
172
|
-
).then((shouldOpenFeedbackPage) {
|
|
173
|
-
if (shouldOpenFeedbackPage == true && ref.context.mounted) {
|
|
174
|
-
ref.context.push("/feedback");
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
return true;
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
class CloseIcon extends StatelessWidget {
|
|
182
|
-
final VoidCallback onExit;
|
|
183
|
-
|
|
184
|
-
const CloseIcon({super.key, required this.onExit});
|
|
185
|
-
|
|
186
|
-
@override
|
|
187
|
-
Widget build(BuildContext context) {
|
|
188
|
-
return ClipOval(
|
|
189
|
-
child: Material(
|
|
190
|
-
color: Colors.transparent,
|
|
191
|
-
child: InkWell(
|
|
192
|
-
onTap: () => onExit.call(),
|
|
193
|
-
child: Ink(
|
|
194
|
-
width: 32,
|
|
195
|
-
height: 32,
|
|
196
|
-
// padding: const EdgeInsets.all(6),
|
|
197
|
-
decoration: BoxDecoration(
|
|
198
|
-
color: context.colors.background,
|
|
199
|
-
shape: BoxShape.circle,
|
|
200
|
-
),
|
|
201
|
-
child: Icon(
|
|
202
|
-
KasyIcons.close,
|
|
203
|
-
color: context.colors.onBackground,
|
|
204
|
-
size: 21,
|
|
205
|
-
),
|
|
206
|
-
),
|
|
207
|
-
),
|
|
208
|
-
),
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// ignore_for_file: invalid_annotation_target, constant_identifier_names
|
|
2
|
-
|
|
3
|
-
import 'package:kasy_kit/features/notifications/api/entities/notifications_entity.dart';
|
|
4
|
-
import 'package:kasy_kit/features/notifications/api/local_notifier.dart';
|
|
5
|
-
import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
|
|
6
|
-
import 'package:go_router/go_router.dart';
|
|
7
|
-
import 'package:kasy_kit/router.dart';
|
|
8
|
-
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
9
|
-
import 'package:logger/logger.dart';
|
|
10
|
-
import 'package:permission_handler/permission_handler.dart';
|
|
11
|
-
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
12
|
-
import 'package:url_launcher/url_launcher.dart';
|
|
13
|
-
|
|
14
|
-
part 'notification.freezed.dart';
|
|
15
|
-
part 'notification.g.dart';
|
|
16
|
-
|
|
17
|
-
@freezed
|
|
18
|
-
sealed class Notification with _$Notification {
|
|
19
|
-
const Notification._();
|
|
20
|
-
|
|
21
|
-
const factory Notification.withData({
|
|
22
|
-
String? id,
|
|
23
|
-
required String title,
|
|
24
|
-
required String body,
|
|
25
|
-
required DateTime createdAt,
|
|
26
|
-
DateTime? readAt,
|
|
27
|
-
String? imageUrl,
|
|
28
|
-
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
29
|
-
LocalNotifier? notifier,
|
|
30
|
-
@JsonKey(includeFromJson: false, includeToJson: false)
|
|
31
|
-
NotificationSettings? notifierSettings,
|
|
32
|
-
NotificationTypes? type,
|
|
33
|
-
Map<String, dynamic>? data,
|
|
34
|
-
}) = NotificationData;
|
|
35
|
-
|
|
36
|
-
factory Notification.fromJson(Map<String, dynamic> json) =>
|
|
37
|
-
_$NotificationFromJson(json);
|
|
38
|
-
|
|
39
|
-
factory Notification.from(
|
|
40
|
-
Map<String, dynamic> json, {
|
|
41
|
-
String? id,
|
|
42
|
-
Map<String, dynamic>? data,
|
|
43
|
-
LocalNotifier? notifierApi,
|
|
44
|
-
NotificationSettings? notifierSettings,
|
|
45
|
-
}) =>
|
|
46
|
-
Notification.withData(
|
|
47
|
-
id: id,
|
|
48
|
-
title: json['title'] as String,
|
|
49
|
-
body: json['body'] as String,
|
|
50
|
-
imageUrl: json['image'] as String?,
|
|
51
|
-
type: data != null && data.containsKey('type')
|
|
52
|
-
? NotificationTypes.values.firstWhere(
|
|
53
|
-
(e) => e.name == data['type'],
|
|
54
|
-
orElse: () => NotificationTypes.OTHER,
|
|
55
|
-
)
|
|
56
|
-
: null,
|
|
57
|
-
data: data,
|
|
58
|
-
createdAt: DateTime.now(),
|
|
59
|
-
notifier: notifierApi,
|
|
60
|
-
notifierSettings: notifierSettings,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
Future<void> show({NotificationSettings? settings}) async {
|
|
64
|
-
if (notifier == null) {
|
|
65
|
-
throw Exception(
|
|
66
|
-
'You must provide a LocalNotifierApi to show a notification',
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
if (notifierSettings != null) {
|
|
70
|
-
await notifier!.show(notifierSettings!, this);
|
|
71
|
-
return;
|
|
72
|
-
} else if (settings != null) {
|
|
73
|
-
await notifier!.show(settings, this);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
throw Exception(
|
|
77
|
-
'You must provide a NotificationSettings to show a notification',
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
bool get seen => readAt != null;
|
|
82
|
-
|
|
83
|
-
Future<void> onTap() async {
|
|
84
|
-
// if the app is not ready, we store the notification in the pending notification handler
|
|
85
|
-
if (navigatorKey.currentContext == null) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Open external URL
|
|
90
|
-
if (type == NotificationTypes.LINK && data?.containsKey('url') == true) {
|
|
91
|
-
try {
|
|
92
|
-
launchUrl(Uri.parse(data!['url'] as String));
|
|
93
|
-
} catch (e, s) {
|
|
94
|
-
Logger().e("error $e");
|
|
95
|
-
Sentry.captureException(e, stackTrace: s);
|
|
96
|
-
}
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Navigate to an internal route sent in the notification data.
|
|
100
|
-
// Example: data: {"route": "/premium"} → opens the premium page.
|
|
101
|
-
if (data?.containsKey('route') == true) {
|
|
102
|
-
final route = data!['route'] as String;
|
|
103
|
-
navigatorKey.currentContext!.go(route);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
// Default: open the notifications list
|
|
107
|
-
navigatorKey.currentContext!.go('/notifications');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
sealed class NotificationPermission {
|
|
112
|
-
/// ask for permission if needed
|
|
113
|
-
Future<void> maybeAsk() async {
|
|
114
|
-
final permission = this;
|
|
115
|
-
switch (permission) {
|
|
116
|
-
case NotificationPermissionWaiting():
|
|
117
|
-
await permission.ask();
|
|
118
|
-
case NotificationPermissionDenied():
|
|
119
|
-
await permission.ask();
|
|
120
|
-
case NotificationPermissionGranted():
|
|
121
|
-
default:
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// we asked for permission and it was granted
|
|
127
|
-
class NotificationPermissionGranted extends NotificationPermission {
|
|
128
|
-
NotificationPermissionGranted();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/// we asked for permission and it was denied
|
|
132
|
-
class NotificationPermissionDenied extends NotificationPermission {
|
|
133
|
-
final NotificationSettings? _notificationSettings;
|
|
134
|
-
final NotificationsRepository? _repository;
|
|
135
|
-
|
|
136
|
-
NotificationPermissionDenied({
|
|
137
|
-
NotificationSettings? notificationSettings,
|
|
138
|
-
NotificationsRepository? repository,
|
|
139
|
-
}) : _repository = repository,
|
|
140
|
-
_notificationSettings = notificationSettings;
|
|
141
|
-
|
|
142
|
-
Future<void> ask() async {
|
|
143
|
-
if (_notificationSettings == null) {
|
|
144
|
-
throw Exception("NotificationsApi is null");
|
|
145
|
-
}
|
|
146
|
-
final granted = await _notificationSettings.askPermission();
|
|
147
|
-
NotificationPermission? permission;
|
|
148
|
-
if (granted) {
|
|
149
|
-
permission = NotificationPermissionGranted();
|
|
150
|
-
} else {
|
|
151
|
-
permission = NotificationPermissionDenied();
|
|
152
|
-
}
|
|
153
|
-
await _repository!.init();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
Future<void> openSettings() async {
|
|
157
|
-
await openAppSettings();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/// we never asked for permission
|
|
162
|
-
class NotificationPermissionWaiting extends NotificationPermission {
|
|
163
|
-
final NotificationSettings? _notificationSettings;
|
|
164
|
-
final NotificationsRepository? _repository;
|
|
165
|
-
|
|
166
|
-
NotificationPermissionWaiting({
|
|
167
|
-
NotificationSettings? notificationSettings,
|
|
168
|
-
NotificationsRepository? repository,
|
|
169
|
-
}) : _notificationSettings = notificationSettings,
|
|
170
|
-
_repository = repository;
|
|
171
|
-
|
|
172
|
-
Future<void> ask() async {
|
|
173
|
-
if (_notificationSettings == null) {
|
|
174
|
-
throw Exception("NotificationsApi is null");
|
|
175
|
-
}
|
|
176
|
-
final granted = await _notificationSettings.askPermission();
|
|
177
|
-
NotificationPermission? permission;
|
|
178
|
-
if (granted) {
|
|
179
|
-
permission = NotificationPermissionGranted();
|
|
180
|
-
await _repository?.init();
|
|
181
|
-
} else {
|
|
182
|
-
permission = NotificationPermissionDenied();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
-
import 'package:kasy_kit/components/components.dart';
|
|
4
|
-
import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
5
|
-
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
|
-
import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
|
|
7
|
-
import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
|
|
8
|
-
import 'package:kasy_kit/features/onboarding/providers/onboarding_provider.dart';
|
|
9
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
|
|
10
|
-
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart';
|
|
11
|
-
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
|
-
|
|
13
|
-
final _formStep3Key = GlobalKey<FormState>();
|
|
14
|
-
|
|
15
|
-
class NotificationsPermissionStep extends ConsumerWidget {
|
|
16
|
-
final String nextRoute;
|
|
17
|
-
|
|
18
|
-
const NotificationsPermissionStep({
|
|
19
|
-
super.key,
|
|
20
|
-
required this.nextRoute,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
@override
|
|
24
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
25
|
-
final notifRepository = ref.watch(notificationRepositoryProvider);
|
|
26
|
-
final permissionFuture = notifRepository.getPermissionStatus();
|
|
27
|
-
final translations = Translations.of(context).onboarding.notifications;
|
|
28
|
-
|
|
29
|
-
return OnboardingBackground(
|
|
30
|
-
child: FutureBuilder<NotificationPermission>(
|
|
31
|
-
future: permissionFuture,
|
|
32
|
-
builder: (context, snapshot) {
|
|
33
|
-
if (!snapshot.hasData) {
|
|
34
|
-
return const SizedBox();
|
|
35
|
-
}
|
|
36
|
-
return Form(
|
|
37
|
-
key: _formStep3Key,
|
|
38
|
-
child: OnboardingIllustrationScaffold(
|
|
39
|
-
progress: 0.9,
|
|
40
|
-
title: translations.title,
|
|
41
|
-
description: translations.description,
|
|
42
|
-
imageAsset: 'assets/images/onboarding/img2.jpg',
|
|
43
|
-
footerActions: [
|
|
44
|
-
KasyButton(
|
|
45
|
-
label: translations.continue_button,
|
|
46
|
-
expand: true,
|
|
47
|
-
onPressed: () => ref.onboardingNotifier
|
|
48
|
-
.setupNotifications()
|
|
49
|
-
.then((_) {
|
|
50
|
-
if (!context.mounted) return;
|
|
51
|
-
Navigator.of(context).pushNamed(nextRoute);
|
|
52
|
-
}),
|
|
53
|
-
),
|
|
54
|
-
const SizedBox(height: KasySpacing.smd),
|
|
55
|
-
KasyButton(
|
|
56
|
-
label: translations.skip_button,
|
|
57
|
-
variant: KasyButtonVariant.soft,
|
|
58
|
-
expand: true,
|
|
59
|
-
onPressed: () {
|
|
60
|
-
ref
|
|
61
|
-
.read(analyticsApiProvider)
|
|
62
|
-
.logEvent('setup_notifications_refused', {});
|
|
63
|
-
Navigator.of(context).pushNamed(nextRoute);
|
|
64
|
-
},
|
|
65
|
-
),
|
|
66
|
-
],
|
|
67
|
-
),
|
|
68
|
-
);
|
|
69
|
-
},
|
|
70
|
-
),
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|