kasy-cli 1.32.0 → 1.35.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 +66 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +61 -11
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +27 -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 +199 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -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 +92 -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/scaffold/shared/generator-utils.js +18 -6
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +85 -0
- package/lib/utils/i18n/messages-es.js +85 -0
- package/lib/utils/i18n/messages-pt.js +85 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +170 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +269 -0
- package/templates/firebase/docs/auth-setup.en.md +4 -2
- package/templates/firebase/docs/auth-setup.es.md +4 -2
- package/templates/firebase/docs/auth-setup.pt.md +4 -2
- 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/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
- 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_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
- 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 +108 -73
- 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/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +5 -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 +48 -124
- 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/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- 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 +266 -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 +80 -15
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
- 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/feedbacks/ui/widgets/feature_card.dart +1 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
- 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/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
- 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/avatar_component.dart +2 -2
- 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 +54 -7
- package/templates/firebase/lib/i18n/es.i18n.json +54 -7
- package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +94 -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/tool/design_check.dart +152 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
|
@@ -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: [
|
|
@@ -1098,13 +1101,23 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1098
1101
|
_ActionCard(
|
|
1099
1102
|
icon: KasyIcons.note,
|
|
1100
1103
|
title: admin.update_bottom_sheet,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1104
|
+
// Preview the sheet over the current screen; dismissing (tap-outside /
|
|
1105
|
+
// Continue) just closes it and returns here, without navigating away.
|
|
1106
|
+
onTap: () => showUpdateBottomSheet(
|
|
1107
|
+
context: navigatorKey.currentContext!,
|
|
1108
|
+
version: '0.0.0',
|
|
1109
|
+
),
|
|
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
|
+
),
|
|
1108
1121
|
),
|
|
1109
1122
|
_ActionCard(
|
|
1110
1123
|
icon: KasyIcons.payment,
|
|
@@ -1124,6 +1137,8 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1124
1137
|
_ActionCard(
|
|
1125
1138
|
icon: KasyIcons.star,
|
|
1126
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.
|
|
1127
1142
|
onTap: () => showReviewDialog(context, ref, force: true),
|
|
1128
1143
|
),
|
|
1129
1144
|
_ActionCard(
|
|
@@ -1141,29 +1156,51 @@ class _ToolsTab extends ConsumerWidget {
|
|
|
1141
1156
|
Clipboard.setData(
|
|
1142
1157
|
ClipboardData(text: userState.user.idOrNull ?? 'no-id (guest)'),
|
|
1143
1158
|
);
|
|
1144
|
-
ref.read(toastProvider).alert(
|
|
1159
|
+
ref.read(toastProvider).alert(
|
|
1160
|
+
title: t.common.copied,
|
|
1161
|
+
text: admin.user_id_copied,
|
|
1162
|
+
);
|
|
1145
1163
|
},
|
|
1146
1164
|
),
|
|
1147
1165
|
_ActionCard(
|
|
1148
1166
|
icon: KasyIcons.notification,
|
|
1149
1167
|
title: admin.copy_fcm_token,
|
|
1150
1168
|
onTap: () async {
|
|
1169
|
+
if (kIsWeb) {
|
|
1170
|
+
ref.read(toastProvider).alert(
|
|
1171
|
+
title: t.common.native_only_title,
|
|
1172
|
+
text: admin.native_only,
|
|
1173
|
+
);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1151
1176
|
final token = await FirebaseMessaging.instance.getToken();
|
|
1152
1177
|
if (token == null) {
|
|
1153
1178
|
ref.read(toastProvider).alert(
|
|
1154
|
-
title:
|
|
1179
|
+
title: t.common.unavailable,
|
|
1155
1180
|
text: admin.fcm_token_unavailable,
|
|
1156
1181
|
);
|
|
1157
1182
|
return;
|
|
1158
1183
|
}
|
|
1159
1184
|
await Clipboard.setData(ClipboardData(text: token));
|
|
1160
|
-
ref.read(toastProvider).alert(
|
|
1185
|
+
ref.read(toastProvider).alert(
|
|
1186
|
+
title: t.common.copied,
|
|
1187
|
+
text: admin.fcm_token_copied,
|
|
1188
|
+
);
|
|
1161
1189
|
},
|
|
1162
1190
|
),
|
|
1163
1191
|
_ActionCard(
|
|
1164
1192
|
icon: KasyIcons.notificationActive,
|
|
1165
1193
|
title: admin.ask_notification,
|
|
1166
|
-
onTap: ()
|
|
1194
|
+
onTap: () {
|
|
1195
|
+
if (kIsWeb) {
|
|
1196
|
+
ref.read(toastProvider).alert(
|
|
1197
|
+
title: t.common.native_only_title,
|
|
1198
|
+
text: admin.native_only,
|
|
1199
|
+
);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
ref.read(notificationsSettingsProvider).askPermission();
|
|
1203
|
+
},
|
|
1167
1204
|
),
|
|
1168
1205
|
];
|
|
1169
1206
|
|
|
@@ -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
|
),
|
|
@@ -97,8 +97,8 @@ class _EditableUserAvatarState extends ConsumerState<EditableUserAvatar> {
|
|
|
97
97
|
child: Container(
|
|
98
98
|
width: 22,
|
|
99
99
|
height: 22,
|
|
100
|
-
decoration:
|
|
101
|
-
color:
|
|
100
|
+
decoration: BoxDecoration(
|
|
101
|
+
color: Colors.black.withValues(alpha: 0.6),
|
|
102
102
|
shape: BoxShape.circle,
|
|
103
103
|
),
|
|
104
104
|
child: const Icon(
|
|
@@ -0,0 +1,141 @@
|
|
|
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/theme/theme.dart';
|
|
5
|
+
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
6
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
7
|
+
|
|
8
|
+
/// Opens the "create password" bottom sheet. Lets a social-only user (Google,
|
|
9
|
+
/// Apple, ...) set a password so they can also sign in with email + password.
|
|
10
|
+
/// The account stays the same (no duplicate). Shows a success toast on the
|
|
11
|
+
/// calling [context] when done.
|
|
12
|
+
Future<void> showCreatePasswordSheet(BuildContext context) async {
|
|
13
|
+
final bool? saved = await showKasyBottomSheet<bool>(
|
|
14
|
+
context: context,
|
|
15
|
+
isScrollControlled: true,
|
|
16
|
+
builder: (_) => const _CreatePasswordSheet(),
|
|
17
|
+
);
|
|
18
|
+
if (saved == true && context.mounted) {
|
|
19
|
+
showKasyToast(
|
|
20
|
+
context,
|
|
21
|
+
title: context.t.settings.create_password_success,
|
|
22
|
+
tone: KasyToastTone.success,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class _CreatePasswordSheet extends ConsumerStatefulWidget {
|
|
28
|
+
const _CreatePasswordSheet();
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
ConsumerState<_CreatePasswordSheet> createState() =>
|
|
32
|
+
_CreatePasswordSheetState();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class _CreatePasswordSheetState extends ConsumerState<_CreatePasswordSheet> {
|
|
36
|
+
final _passwordController = TextEditingController();
|
|
37
|
+
final _confirmController = TextEditingController();
|
|
38
|
+
bool _saving = false;
|
|
39
|
+
String? _error;
|
|
40
|
+
|
|
41
|
+
static const int _minLength = 6;
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
void dispose() {
|
|
45
|
+
_passwordController.dispose();
|
|
46
|
+
_confirmController.dispose();
|
|
47
|
+
super.dispose();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Future<void> _save() async {
|
|
51
|
+
if (_saving) return;
|
|
52
|
+
final tr = context.t.settings;
|
|
53
|
+
final password = _passwordController.text;
|
|
54
|
+
if (password.length < _minLength) {
|
|
55
|
+
setState(() => _error = tr.create_password_too_short);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (password != _confirmController.text) {
|
|
59
|
+
setState(() => _error = tr.create_password_mismatch);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
setState(() {
|
|
63
|
+
_saving = true;
|
|
64
|
+
_error = null;
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
await ref.read(authRepositoryProvider).setPassword(password);
|
|
68
|
+
if (!mounted) return;
|
|
69
|
+
// Toast is shown by the launcher on the page context (this sheet's context
|
|
70
|
+
// is gone right after the pop).
|
|
71
|
+
Navigator.of(context).pop(true);
|
|
72
|
+
} catch (_) {
|
|
73
|
+
if (!mounted) return;
|
|
74
|
+
setState(() => _saving = false);
|
|
75
|
+
showKasyToast(
|
|
76
|
+
context,
|
|
77
|
+
title: tr.create_password_error,
|
|
78
|
+
tone: KasyToastTone.danger,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
Widget build(BuildContext context) {
|
|
85
|
+
final tr = context.t.settings;
|
|
86
|
+
return KasyBottomSheet(
|
|
87
|
+
title: tr.create_password_title,
|
|
88
|
+
addKeyboardInset: true,
|
|
89
|
+
body: Column(
|
|
90
|
+
mainAxisSize: MainAxisSize.min,
|
|
91
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
92
|
+
children: [
|
|
93
|
+
Text(
|
|
94
|
+
tr.create_password_subtitle,
|
|
95
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
96
|
+
color: context.colors.muted,
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
const SizedBox(height: KasySpacing.md),
|
|
100
|
+
KasyTextField(
|
|
101
|
+
controller: _passwordController,
|
|
102
|
+
label: tr.create_password_field,
|
|
103
|
+
contentType: KasyTextFieldContentType.password,
|
|
104
|
+
textInputAction: TextInputAction.next,
|
|
105
|
+
),
|
|
106
|
+
const SizedBox(height: KasySpacing.sm),
|
|
107
|
+
KasyTextField(
|
|
108
|
+
controller: _confirmController,
|
|
109
|
+
label: tr.create_password_confirm_label,
|
|
110
|
+
contentType: KasyTextFieldContentType.password,
|
|
111
|
+
textInputAction: TextInputAction.done,
|
|
112
|
+
onSubmitted: (_) => _save(),
|
|
113
|
+
),
|
|
114
|
+
if (_error != null) ...[
|
|
115
|
+
const SizedBox(height: KasySpacing.sm),
|
|
116
|
+
Text(
|
|
117
|
+
_error!,
|
|
118
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
119
|
+
color: context.colors.error,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
],
|
|
123
|
+
],
|
|
124
|
+
),
|
|
125
|
+
actions: [
|
|
126
|
+
KasyButton(
|
|
127
|
+
label: tr.edit_name_save,
|
|
128
|
+
expand: true,
|
|
129
|
+
isLoading: _saving,
|
|
130
|
+
onPressed: _save,
|
|
131
|
+
),
|
|
132
|
+
KasyButton(
|
|
133
|
+
label: tr.edit_name_cancel,
|
|
134
|
+
variant: KasyButtonVariant.ghost,
|
|
135
|
+
expand: true,
|
|
136
|
+
onPressed: _saving ? null : () => Navigator.of(context).pop(),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|