kasy-cli 1.34.0 → 1.36.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 +1 -1
- package/bin/kasy.js +24 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/new.js +11 -9
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +18 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
- package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/generate.js +53 -4
- package/lib/utils/i18n/messages-en.js +23 -0
- package/lib/utils/i18n/messages-es.js +23 -0
- package/lib/utils/i18n/messages-pt.js +23 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +83 -0
- package/templates/firebase/DESIGN_SYSTEM.md +37 -2
- package/templates/firebase/docs/auth-setup.en.md +2 -0
- package/templates/firebase/docs/auth-setup.es.md +2 -0
- package/templates/firebase/docs/auth-setup.pt.md +2 -0
- package/templates/firebase/firebase.json +56 -1
- package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
- package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_sidebar.dart +215 -178
- package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
- package/templates/firebase/lib/components/kasy_toast.dart +107 -41
- package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
- package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
- package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
- package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
- package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
- package/templates/firebase/lib/core/guards/guard.dart +16 -2
- package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
- package/templates/firebase/lib/core/states/logout_action.dart +5 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
- package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
- package/templates/firebase/lib/core/theme/texts.dart +90 -57
- package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
- package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
- package/templates/firebase/lib/core/web_screen_width.dart +15 -0
- package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
- package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
- package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
- package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +226 -105
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
- package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
- package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
- package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
- package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
- package/templates/firebase/lib/i18n/en.i18n.json +49 -3
- package/templates/firebase/lib/i18n/es.i18n.json +49 -3
- package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +92 -13
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
- package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
|
@@ -1,15 +1,26 @@
|
|
|
1
|
+
import 'package:kasy_kit/features/onboarding/models/user_info.dart';
|
|
2
|
+
|
|
1
3
|
class OnboardingState {
|
|
2
4
|
DateTime? reminder;
|
|
3
5
|
|
|
6
|
+
/// Answers collected before the anonymous account exists. The account is now
|
|
7
|
+
/// created lazily at the end of onboarding (the loader screen), so the
|
|
8
|
+
/// question screens have no user id to write to yet — we buffer the answers
|
|
9
|
+
/// here and flush them once the account is created.
|
|
10
|
+
final List<UserInfoDetail> pendingUserInfo;
|
|
11
|
+
|
|
4
12
|
OnboardingState({
|
|
5
13
|
this.reminder,
|
|
14
|
+
this.pendingUserInfo = const [],
|
|
6
15
|
});
|
|
7
16
|
|
|
8
17
|
OnboardingState copyWith({
|
|
9
18
|
DateTime? reminder,
|
|
19
|
+
List<UserInfoDetail>? pendingUserInfo,
|
|
10
20
|
}) {
|
|
11
21
|
return OnboardingState(
|
|
12
22
|
reminder: reminder ?? this.reminder,
|
|
23
|
+
pendingUserInfo: pendingUserInfo ?? this.pendingUserInfo,
|
|
13
24
|
);
|
|
14
25
|
}
|
|
15
26
|
}
|
|
@@ -31,8 +31,18 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
31
31
|
|
|
32
32
|
Future<void> onAnsweredQuestion(UserInfoDetail value) async {
|
|
33
33
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
34
|
-
if (userId
|
|
35
|
-
|
|
34
|
+
if (userId != null) {
|
|
35
|
+
// Account already exists (e.g. a returning guest re-onboarding): save now.
|
|
36
|
+
await ref.read(userInfosRepositoryProvider).save(userId, value);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// No account yet — it's created at the end of onboarding. Buffer the answer
|
|
40
|
+
// (replacing any previous answer of the same kind) so it isn't lost; it's
|
|
41
|
+
// flushed in [onOnboardingCompleted] once the account exists.
|
|
42
|
+
final others = state.pendingUserInfo
|
|
43
|
+
.where((info) => info.runtimeType != value.runtimeType)
|
|
44
|
+
.toList();
|
|
45
|
+
state = state.copyWith(pendingUserInfo: [...others, value]);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
Future<void> setupNotifications() async {
|
|
@@ -66,7 +76,24 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
66
76
|
Future<void> onOnboardingCompleted() async {
|
|
67
77
|
final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
|
|
68
78
|
|
|
69
|
-
|
|
79
|
+
// This is the "preparing everything for you" moment (the loader screen):
|
|
80
|
+
// the only place the anonymous guest account gets created. Doing it here —
|
|
81
|
+
// instead of eagerly on app start — means a fresh anonymous account is born
|
|
82
|
+
// only when the user actually commits to using the app.
|
|
83
|
+
await userStateNotifier.continueAsGuest();
|
|
84
|
+
|
|
85
|
+
// Now that the account exists, flush the answers collected during the
|
|
86
|
+
// questions (gender, age, …) that had nowhere to go before.
|
|
87
|
+
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
88
|
+
final pending = state.pendingUserInfo;
|
|
89
|
+
if (userId != null && pending.isNotEmpty) {
|
|
90
|
+
final repository = ref.read(userInfosRepositoryProvider);
|
|
91
|
+
for (final info in pending) {
|
|
92
|
+
await repository.save(userId, info);
|
|
93
|
+
}
|
|
94
|
+
state = state.copyWith(pendingUserInfo: const []);
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
await userStateNotifier.refresh();
|
|
71
98
|
}
|
|
72
99
|
|
|
@@ -20,10 +20,19 @@ class _OnboardingJournalLoaderState extends ConsumerState<OnboardingLoader> {
|
|
|
20
20
|
@override
|
|
21
21
|
void initState() {
|
|
22
22
|
super.initState();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
_prepare();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Future<void> _prepare() async {
|
|
27
|
+
// Run the real work (which now lazily creates the guest account) while the
|
|
28
|
+
// loader animation plays, so its network latency is hidden under the
|
|
29
|
+
// minimum display time instead of being added on top of it.
|
|
30
|
+
await Future.wait([
|
|
31
|
+
ref.onboardingNotifier.onOnboardingCompleted(),
|
|
32
|
+
Future<void>.delayed(const Duration(milliseconds: 3500)),
|
|
33
|
+
]);
|
|
34
|
+
if (!mounted) return;
|
|
35
|
+
widget.onCompleted();
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
@override
|
|
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
|
|
5
5
|
import 'package:kasy_kit/components/components.dart';
|
|
6
6
|
import 'package:kasy_kit/core/chrome/chrome_visibility.dart';
|
|
7
7
|
import 'package:kasy_kit/core/config/features.dart';
|
|
8
|
+
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
8
9
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
9
10
|
import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
|
|
10
11
|
import 'package:kasy_kit/core/security/biometric_preference_notifier.dart';
|
|
@@ -14,7 +15,10 @@ import 'package:kasy_kit/core/states/logout_action.dart';
|
|
|
14
15
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
15
16
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
16
17
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
18
|
+
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
19
|
+
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
17
20
|
import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
|
|
21
|
+
import 'package:kasy_kit/features/settings/ui/components/create_password_sheet.dart';
|
|
18
22
|
import 'package:kasy_kit/features/settings/ui/components/delete_user_component.dart';
|
|
19
23
|
import 'package:kasy_kit/features/settings/ui/components/edit_name_sheet.dart';
|
|
20
24
|
import 'package:kasy_kit/features/settings/ui/components/language_switcher.dart';
|
|
@@ -23,6 +27,60 @@ import 'package:kasy_kit/i18n/translations.g.dart';
|
|
|
23
27
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
24
28
|
import 'package:url_launcher/url_launcher.dart';
|
|
25
29
|
|
|
30
|
+
/// All providers linked to the current account (google/apple/facebook/email/
|
|
31
|
+
/// phone), for the "Connected with" row and to decide whether to offer "create
|
|
32
|
+
/// password". Empty for guests/unknown.
|
|
33
|
+
final _linkedProvidersProvider = FutureProvider.autoDispose<List<String>>(
|
|
34
|
+
(ref) => ref.read(authRepositoryProvider).getLinkedProviders(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/// Social providers the current user can still link to their account (Firebase).
|
|
38
|
+
/// Empty on backends that link automatically (Supabase) or aren't wired (API).
|
|
39
|
+
final _linkableProvidersProvider = FutureProvider.autoDispose<List<String>>(
|
|
40
|
+
(ref) => ref.read(authRepositoryProvider).linkableSocialProviders(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
String _providerDisplayName(String provider) => switch (provider) {
|
|
44
|
+
'google' => 'Google',
|
|
45
|
+
'apple' => 'Apple',
|
|
46
|
+
'facebook' => 'Facebook',
|
|
47
|
+
_ => provider,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/// Links a social provider to the current account, then refreshes the lists and
|
|
51
|
+
/// shows a toast. Cancelled flows are silently ignored.
|
|
52
|
+
Future<void> _linkSocialProvider(
|
|
53
|
+
WidgetRef ref,
|
|
54
|
+
BuildContext context,
|
|
55
|
+
String provider,
|
|
56
|
+
) async {
|
|
57
|
+
final tr = context.t.settings;
|
|
58
|
+
try {
|
|
59
|
+
await ref.read(authRepositoryProvider).linkSocialProvider(provider);
|
|
60
|
+
ref.invalidate(_linkableProvidersProvider);
|
|
61
|
+
ref.invalidate(_linkedProvidersProvider);
|
|
62
|
+
await ref.read(userStateNotifierProvider.notifier).refresh();
|
|
63
|
+
} on UserCancelledSignInException {
|
|
64
|
+
return;
|
|
65
|
+
} catch (_) {
|
|
66
|
+
if (context.mounted) {
|
|
67
|
+
showKasyToast(
|
|
68
|
+
context,
|
|
69
|
+
title: tr.link_social_error,
|
|
70
|
+
tone: KasyToastTone.danger,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (context.mounted) {
|
|
76
|
+
showKasyToast(
|
|
77
|
+
context,
|
|
78
|
+
title: tr.link_social_success(provider: _providerDisplayName(provider)),
|
|
79
|
+
tone: KasyToastTone.success,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
26
84
|
class SettingsPage extends ConsumerWidget {
|
|
27
85
|
const SettingsPage({super.key});
|
|
28
86
|
|
|
@@ -76,6 +134,18 @@ class SettingsPage extends ConsumerWidget {
|
|
|
76
134
|
email: displayEmail,
|
|
77
135
|
isAuthenticated: isAuthenticated,
|
|
78
136
|
onRegister: () => context.push('/signup'),
|
|
137
|
+
linkedProviders: ref
|
|
138
|
+
.watch(_linkedProvidersProvider)
|
|
139
|
+
.asData
|
|
140
|
+
?.value ??
|
|
141
|
+
const [],
|
|
142
|
+
linkableProviders: ref
|
|
143
|
+
.watch(_linkableProvidersProvider)
|
|
144
|
+
.asData
|
|
145
|
+
?.value ??
|
|
146
|
+
const [],
|
|
147
|
+
onLinkProvider: (p) =>
|
|
148
|
+
_linkSocialProvider(ref, context, p),
|
|
79
149
|
),
|
|
80
150
|
const SizedBox(height: KasySpacing.xl),
|
|
81
151
|
..._sections(
|
|
@@ -204,12 +274,8 @@ List<Widget> _preferenceRows(BuildContext context, {required bool isPhone}) {
|
|
|
204
274
|
title: tr.feedback,
|
|
205
275
|
onTap: () => context.push('/feedback'),
|
|
206
276
|
),
|
|
207
|
-
if (withRevenuecat)
|
|
208
|
-
|
|
209
|
-
icon: KasyIcons.payment,
|
|
210
|
-
title: tr.premium,
|
|
211
|
-
onTap: () => context.push('/premium'),
|
|
212
|
-
),
|
|
277
|
+
if (withRevenuecat || withStripe)
|
|
278
|
+
const _BillingTile(),
|
|
213
279
|
];
|
|
214
280
|
}
|
|
215
281
|
|
|
@@ -241,11 +307,20 @@ List<Widget> _accountFields(
|
|
|
241
307
|
required String email,
|
|
242
308
|
required bool isAuthenticated,
|
|
243
309
|
required VoidCallback onRegister,
|
|
310
|
+
List<String> linkedProviders = const [],
|
|
311
|
+
List<String> linkableProviders = const [],
|
|
312
|
+
void Function(String provider)? onLinkProvider,
|
|
244
313
|
}) {
|
|
245
314
|
final tr = context.t.settings;
|
|
246
315
|
if (!isAuthenticated) {
|
|
247
316
|
return [
|
|
248
|
-
KasyButton(
|
|
317
|
+
KasyButton(
|
|
318
|
+
label: tr.register,
|
|
319
|
+
expand: true,
|
|
320
|
+
variant: KasyButtonVariant.ghost,
|
|
321
|
+
foregroundColor: context.colors.primary,
|
|
322
|
+
onPressed: onRegister,
|
|
323
|
+
),
|
|
249
324
|
];
|
|
250
325
|
}
|
|
251
326
|
return [
|
|
@@ -263,10 +338,72 @@ List<Widget> _accountFields(
|
|
|
263
338
|
),
|
|
264
339
|
),
|
|
265
340
|
_FieldRow(label: tr.email_label, value: email),
|
|
341
|
+
if (linkedProviders.isNotEmpty)
|
|
342
|
+
_FieldRow(
|
|
343
|
+
label: tr.connected_with_label,
|
|
344
|
+
value: linkedProviders
|
|
345
|
+
.map((p) => switch (p) {
|
|
346
|
+
'google' => 'Google',
|
|
347
|
+
'apple' => 'Apple',
|
|
348
|
+
'facebook' => 'Facebook',
|
|
349
|
+
'phone' => tr.provider_phone,
|
|
350
|
+
_ => tr.provider_email,
|
|
351
|
+
})
|
|
352
|
+
.join(', '),
|
|
353
|
+
),
|
|
354
|
+
// Social-only accounts can attach a password (same account) so they can
|
|
355
|
+
// also sign in with email + password. Email/phone users already have one.
|
|
356
|
+
if (linkedProviders.isNotEmpty &&
|
|
357
|
+
!linkedProviders.contains('email') &&
|
|
358
|
+
!linkedProviders.contains('phone'))
|
|
359
|
+
_FieldRow(
|
|
360
|
+
label: tr.create_password_title,
|
|
361
|
+
value: '',
|
|
362
|
+
onTap: () => showCreatePasswordSheet(context),
|
|
363
|
+
),
|
|
364
|
+
// Firebase: link a social provider (Google/Apple) to this account so it
|
|
365
|
+
// can also be used to sign in. Empty/hidden on Supabase (auto-links).
|
|
366
|
+
for (final provider in linkableProviders)
|
|
367
|
+
_FieldRow(
|
|
368
|
+
label: tr.link_social(provider: _providerDisplayName(provider)),
|
|
369
|
+
value: '',
|
|
370
|
+
onTap:
|
|
371
|
+
onLinkProvider == null ? null : () => onLinkProvider(provider),
|
|
372
|
+
),
|
|
266
373
|
]),
|
|
267
374
|
];
|
|
268
375
|
}
|
|
269
376
|
|
|
377
|
+
/// Subscription/billing entry in the Preferences section.
|
|
378
|
+
/// - Not subscribed → "Premium" (taps into the paywall).
|
|
379
|
+
/// - Subscribed → "Cobrança" + plan name on the right (taps into billing).
|
|
380
|
+
class _BillingTile extends ConsumerWidget {
|
|
381
|
+
const _BillingTile();
|
|
382
|
+
|
|
383
|
+
@override
|
|
384
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
385
|
+
final sub = ref.watch(userStateNotifierProvider).subscription;
|
|
386
|
+
final tr = context.t;
|
|
387
|
+
|
|
388
|
+
if (sub is SubscriptionStateData) {
|
|
389
|
+
final planName =
|
|
390
|
+
sub.activeOffer?.title ?? tr.activePremium.plan_fallback;
|
|
391
|
+
return SettingsTile(
|
|
392
|
+
icon: KasyIcons.payment,
|
|
393
|
+
title: tr.settings.billing,
|
|
394
|
+
trailingLabel: planName,
|
|
395
|
+
onTap: () => context.push('/premium'),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return SettingsTile(
|
|
400
|
+
icon: KasyIcons.payment,
|
|
401
|
+
title: tr.settings.premium,
|
|
402
|
+
onTap: () => context.push('/premium'),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
270
407
|
class _SectionLabel extends StatelessWidget {
|
|
271
408
|
final String label;
|
|
272
409
|
const _SectionLabel(this.label);
|
|
@@ -279,7 +416,7 @@ class _SectionLabel extends StatelessWidget {
|
|
|
279
416
|
padding: const EdgeInsets.only(left: KasySpacing.xs),
|
|
280
417
|
child: Text(
|
|
281
418
|
label,
|
|
282
|
-
style:
|
|
419
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
283
420
|
color: context.colors.muted,
|
|
284
421
|
),
|
|
285
422
|
),
|
|
@@ -544,9 +681,8 @@ class _DesktopNav extends StatelessWidget {
|
|
|
544
681
|
name,
|
|
545
682
|
maxLines: 1,
|
|
546
683
|
overflow: TextOverflow.ellipsis,
|
|
547
|
-
style: context.textTheme.
|
|
684
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
548
685
|
color: context.colors.onSurface,
|
|
549
|
-
fontWeight: FontWeight.w600,
|
|
550
686
|
),
|
|
551
687
|
),
|
|
552
688
|
if (email.isNotEmpty)
|
|
@@ -664,7 +800,7 @@ class _DesktopDetail extends ConsumerWidget {
|
|
|
664
800
|
),
|
|
665
801
|
child: Text(
|
|
666
802
|
title,
|
|
667
|
-
style:
|
|
803
|
+
style: context.kasyTextTheme.sectionTitle.copyWith(
|
|
668
804
|
color: context.colors.onSurface,
|
|
669
805
|
),
|
|
670
806
|
),
|
|
@@ -719,6 +855,11 @@ class _DesktopDetail extends ConsumerWidget {
|
|
|
719
855
|
email: email,
|
|
720
856
|
isAuthenticated: isAuthenticated,
|
|
721
857
|
onRegister: () => context.push('/signup'),
|
|
858
|
+
linkedProviders:
|
|
859
|
+
ref.watch(_linkedProvidersProvider).asData?.value ?? const [],
|
|
860
|
+
linkableProviders:
|
|
861
|
+
ref.watch(_linkableProvidersProvider).asData?.value ?? const [],
|
|
862
|
+
onLinkProvider: (p) => _linkSocialProvider(ref, context, p),
|
|
722
863
|
),
|
|
723
864
|
if (isAuthenticated) ...[
|
|
724
865
|
const SizedBox(height: KasySpacing.xl),
|
|
@@ -10,6 +10,7 @@ import 'package:kasy_kit/components/kasy_button.dart';
|
|
|
10
10
|
import 'package:kasy_kit/components/kasy_status_tag.dart';
|
|
11
11
|
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
12
12
|
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
13
|
+
import 'package:kasy_kit/core/app_update/update_available_sheet.dart';
|
|
13
14
|
import 'package:kasy_kit/core/config/features.dart';
|
|
14
15
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
15
16
|
import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
|
|
@@ -159,6 +160,9 @@ class _TabScroll extends StatelessWidget {
|
|
|
159
160
|
@override
|
|
160
161
|
Widget build(BuildContext context) {
|
|
161
162
|
return SingleChildScrollView(
|
|
163
|
+
// Scrolling a form means the user is done with the focused field, so
|
|
164
|
+
// dismiss the keyboard to free up the screen.
|
|
165
|
+
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
162
166
|
child: _MaxWidth(
|
|
163
167
|
child: Padding(
|
|
164
168
|
padding: EdgeInsets.fromLTRB(
|
|
@@ -191,11 +195,8 @@ class _GroupLabel extends StatelessWidget {
|
|
|
191
195
|
),
|
|
192
196
|
child: Text(
|
|
193
197
|
label.toUpperCase(),
|
|
194
|
-
style: context.
|
|
195
|
-
fontSize: 12,
|
|
198
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
196
199
|
color: context.colors.muted,
|
|
197
|
-
letterSpacing: 1.2,
|
|
198
|
-
fontWeight: FontWeight.w700,
|
|
199
200
|
),
|
|
200
201
|
),
|
|
201
202
|
);
|
|
@@ -489,7 +490,7 @@ class _OverviewTab extends ConsumerWidget {
|
|
|
489
490
|
onTap: () {
|
|
490
491
|
Clipboard.setData(ClipboardData(text: uid));
|
|
491
492
|
ref.read(toastProvider).alert(
|
|
492
|
-
title:
|
|
493
|
+
title: t.common.copied,
|
|
493
494
|
text: t.settings.admin.user_id_copied,
|
|
494
495
|
);
|
|
495
496
|
},
|
|
@@ -558,9 +559,8 @@ class _InfoRow extends StatelessWidget {
|
|
|
558
559
|
Expanded(
|
|
559
560
|
child: Text(
|
|
560
561
|
value,
|
|
561
|
-
style: context.
|
|
562
|
+
style: context.kasyTextTheme.rowValue.copyWith(
|
|
562
563
|
color: valueColor ?? context.colors.onSurface,
|
|
563
|
-
fontWeight: FontWeight.w600,
|
|
564
564
|
),
|
|
565
565
|
),
|
|
566
566
|
),
|
|
@@ -787,7 +787,7 @@ class _RequestCard extends ConsumerWidget {
|
|
|
787
787
|
.setActive(req.id!, v);
|
|
788
788
|
ref.invalidate(_adminRequestsProvider);
|
|
789
789
|
if (context.mounted) {
|
|
790
|
-
ref.read(toastProvider).alert(title:
|
|
790
|
+
ref.read(toastProvider).alert(title: t.common.saved, text: r.saved);
|
|
791
791
|
}
|
|
792
792
|
},
|
|
793
793
|
),
|
|
@@ -910,11 +910,11 @@ class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
|
|
|
910
910
|
ref.invalidate(_adminRequestsProvider);
|
|
911
911
|
if (!mounted) return;
|
|
912
912
|
context.pop();
|
|
913
|
-
ref.read(toastProvider).alert(title:
|
|
913
|
+
ref.read(toastProvider).alert(title: t.common.saved, text: r.saved);
|
|
914
914
|
} catch (_) {
|
|
915
915
|
if (!mounted) return;
|
|
916
916
|
setState(() => _saving = false);
|
|
917
|
-
ref.read(toastProvider).alert(title:
|
|
917
|
+
ref.read(toastProvider).alert(title: t.common.error, text: r.error);
|
|
918
918
|
}
|
|
919
919
|
}
|
|
920
920
|
|
|
@@ -927,6 +927,9 @@ class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
|
|
|
927
927
|
body: SizedBox(
|
|
928
928
|
height: MediaQuery.sizeOf(context).height * 0.5,
|
|
929
929
|
child: SingleChildScrollView(
|
|
930
|
+
// Scrolling a form means the user is done with the focused field, so
|
|
931
|
+
// dismiss the keyboard to free up the screen.
|
|
932
|
+
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
930
933
|
child: Column(
|
|
931
934
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
932
935
|
children: [
|
|
@@ -1105,6 +1108,17 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1105
1108
|
version: '0.0.0',
|
|
1106
1109
|
),
|
|
1107
1110
|
),
|
|
1111
|
+
_ActionCard(
|
|
1112
|
+
icon: KasyIcons.download,
|
|
1113
|
+
title: admin.preview_update_available,
|
|
1114
|
+
// Previews the optional (dismissible) sheet so the design can be
|
|
1115
|
+
// reviewed without TestFlight or Remote Config. The forced variant is
|
|
1116
|
+
// the same layout, blocking — test it via app_min_version.
|
|
1117
|
+
onTap: () => showUpdateAvailableSheet(
|
|
1118
|
+
navigatorKey.currentContext!,
|
|
1119
|
+
forced: false,
|
|
1120
|
+
),
|
|
1121
|
+
),
|
|
1108
1122
|
_ActionCard(
|
|
1109
1123
|
icon: KasyIcons.payment,
|
|
1110
1124
|
title: admin.paywalls,
|
|
@@ -1123,6 +1137,8 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1123
1137
|
_ActionCard(
|
|
1124
1138
|
icon: KasyIcons.star,
|
|
1125
1139
|
title: admin.ask_review,
|
|
1140
|
+
// Has a design (the review dialog), so it's previewable on web too —
|
|
1141
|
+
// only the store action no-ops there.
|
|
1126
1142
|
onTap: () => showReviewDialog(context, ref, force: true),
|
|
1127
1143
|
),
|
|
1128
1144
|
_ActionCard(
|
|
@@ -1140,7 +1156,10 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1140
1156
|
Clipboard.setData(
|
|
1141
1157
|
ClipboardData(text: userState.user.idOrNull ?? 'no-id (guest)'),
|
|
1142
1158
|
);
|
|
1143
|
-
ref.read(toastProvider).alert(
|
|
1159
|
+
ref.read(toastProvider).alert(
|
|
1160
|
+
title: t.common.copied,
|
|
1161
|
+
text: admin.user_id_copied,
|
|
1162
|
+
);
|
|
1144
1163
|
},
|
|
1145
1164
|
),
|
|
1146
1165
|
_ActionCard(
|
|
@@ -1148,19 +1167,25 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1148
1167
|
title: admin.copy_fcm_token,
|
|
1149
1168
|
onTap: () async {
|
|
1150
1169
|
if (kIsWeb) {
|
|
1151
|
-
ref.read(toastProvider).alert(
|
|
1170
|
+
ref.read(toastProvider).alert(
|
|
1171
|
+
title: t.common.native_only_title,
|
|
1172
|
+
text: admin.native_only,
|
|
1173
|
+
);
|
|
1152
1174
|
return;
|
|
1153
1175
|
}
|
|
1154
1176
|
final token = await FirebaseMessaging.instance.getToken();
|
|
1155
1177
|
if (token == null) {
|
|
1156
1178
|
ref.read(toastProvider).alert(
|
|
1157
|
-
title:
|
|
1179
|
+
title: t.common.unavailable,
|
|
1158
1180
|
text: admin.fcm_token_unavailable,
|
|
1159
1181
|
);
|
|
1160
1182
|
return;
|
|
1161
1183
|
}
|
|
1162
1184
|
await Clipboard.setData(ClipboardData(text: token));
|
|
1163
|
-
ref.read(toastProvider).alert(
|
|
1185
|
+
ref.read(toastProvider).alert(
|
|
1186
|
+
title: t.common.copied,
|
|
1187
|
+
text: admin.fcm_token_copied,
|
|
1188
|
+
);
|
|
1164
1189
|
},
|
|
1165
1190
|
),
|
|
1166
1191
|
_ActionCard(
|
|
@@ -1168,7 +1193,10 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1168
1193
|
title: admin.ask_notification,
|
|
1169
1194
|
onTap: () {
|
|
1170
1195
|
if (kIsWeb) {
|
|
1171
|
-
ref.read(toastProvider).alert(
|
|
1196
|
+
ref.read(toastProvider).alert(
|
|
1197
|
+
title: t.common.native_only_title,
|
|
1198
|
+
text: admin.native_only,
|
|
1199
|
+
);
|
|
1172
1200
|
return;
|
|
1173
1201
|
}
|
|
1174
1202
|
ref.read(notificationsSettingsProvider).askPermission();
|
|
@@ -613,11 +613,9 @@ class _HeaderCell extends StatelessWidget {
|
|
|
613
613
|
label.toUpperCase(),
|
|
614
614
|
maxLines: 1,
|
|
615
615
|
overflow: TextOverflow.ellipsis,
|
|
616
|
-
style: context.
|
|
616
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
617
617
|
color: color,
|
|
618
618
|
letterSpacing: 0.8,
|
|
619
|
-
fontWeight: FontWeight.w700,
|
|
620
|
-
fontSize: 10.5,
|
|
621
619
|
),
|
|
622
620
|
),
|
|
623
621
|
),
|
|
@@ -666,6 +664,9 @@ class _TableBody extends StatelessWidget {
|
|
|
666
664
|
children: [
|
|
667
665
|
Expanded(
|
|
668
666
|
child: ListView.separated(
|
|
667
|
+
// Scrolling the results means the user stopped typing in the search
|
|
668
|
+
// field above, so dismiss the keyboard to show more of the list.
|
|
669
|
+
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
|
669
670
|
padding: const EdgeInsets.only(bottom: KasySpacing.xs),
|
|
670
671
|
itemCount: users.length,
|
|
671
672
|
separatorBuilder: (_, _) => Divider(
|
|
@@ -957,15 +958,14 @@ class _UserRow extends StatelessWidget {
|
|
|
957
958
|
primaryText,
|
|
958
959
|
maxLines: 1,
|
|
959
960
|
overflow: TextOverflow.ellipsis,
|
|
960
|
-
style:
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
),
|
|
961
|
+
style: isAnonymous
|
|
962
|
+
? context.textTheme.bodyMedium?.copyWith(
|
|
963
|
+
color: context.colors.muted,
|
|
964
|
+
fontStyle: FontStyle.italic,
|
|
965
|
+
)
|
|
966
|
+
: context.kasyTextTheme.rowTitle.copyWith(
|
|
967
|
+
color: context.colors.onSurface,
|
|
968
|
+
),
|
|
969
969
|
),
|
|
970
970
|
if (subText != null) ...[
|
|
971
971
|
const SizedBox(height: 1),
|
|
@@ -182,11 +182,8 @@ class _SendPushNotificationPageState
|
|
|
182
182
|
),
|
|
183
183
|
child: Text(
|
|
184
184
|
tr.send_push_preview_label.toUpperCase(),
|
|
185
|
-
style: context.
|
|
185
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
186
186
|
color: context.colors.muted,
|
|
187
|
-
letterSpacing: 1.2,
|
|
188
|
-
fontWeight: FontWeight.w700,
|
|
189
|
-
fontSize: 12,
|
|
190
187
|
),
|
|
191
188
|
),
|
|
192
189
|
),
|