kasy-cli 1.34.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 +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 +9 -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 +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 +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 +154 -56
- 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,4 +1,5 @@
|
|
|
1
1
|
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
|
2
|
+
import 'package:flutter/foundation.dart' show kDebugMode;
|
|
2
3
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
4
|
import 'package:kasy_kit/core/initializer/onstart_service.dart';
|
|
4
5
|
import 'package:logger/logger.dart';
|
|
@@ -17,6 +18,18 @@ final remoteConfigApiProvider = Provider<RemoteConfigApi>(
|
|
|
17
18
|
api: remoteConfigWrapper,
|
|
18
19
|
),
|
|
19
20
|
),
|
|
21
|
+
appUpdate: AppUpdateConfigs(
|
|
22
|
+
latestVersion: FirebaseRemoteConfigData<String>(
|
|
23
|
+
key: 'app_latest_version',
|
|
24
|
+
api: remoteConfigWrapper,
|
|
25
|
+
defaultValue: '0.0.0',
|
|
26
|
+
),
|
|
27
|
+
minVersion: FirebaseRemoteConfigData<String>(
|
|
28
|
+
key: 'app_min_version',
|
|
29
|
+
api: remoteConfigWrapper,
|
|
30
|
+
defaultValue: '0.0.0',
|
|
31
|
+
),
|
|
32
|
+
),
|
|
20
33
|
);
|
|
21
34
|
},
|
|
22
35
|
);
|
|
@@ -48,10 +61,12 @@ final remoteConfigApiProvider = Provider<RemoteConfigApi>(
|
|
|
48
61
|
class RemoteConfigApi implements OnStartService {
|
|
49
62
|
final FirebaseRemoteConfig _remoteConfig;
|
|
50
63
|
final SubscriptionConfigs subscription; // this is an example of group of keys
|
|
64
|
+
final AppUpdateConfigs appUpdate;
|
|
51
65
|
|
|
52
66
|
RemoteConfigApi({
|
|
53
67
|
required FirebaseRemoteConfig remoteConfig,
|
|
54
68
|
required this.subscription,
|
|
69
|
+
required this.appUpdate,
|
|
55
70
|
}) : _remoteConfig = remoteConfig;
|
|
56
71
|
|
|
57
72
|
@override
|
|
@@ -59,7 +74,11 @@ class RemoteConfigApi implements OnStartService {
|
|
|
59
74
|
try {
|
|
60
75
|
await _remoteConfig.setConfigSettings(RemoteConfigSettings(
|
|
61
76
|
fetchTimeout: const Duration(seconds: 60),
|
|
62
|
-
|
|
77
|
+
// In debug we fetch every launch so config changes (e.g. testing the
|
|
78
|
+
// "update available" versions) show up immediately; release throttles
|
|
79
|
+
// to once an hour as recommended.
|
|
80
|
+
minimumFetchInterval:
|
|
81
|
+
kDebugMode ? Duration.zero : const Duration(hours: 1),
|
|
63
82
|
));
|
|
64
83
|
await _remoteConfig.fetchAndActivate();
|
|
65
84
|
} catch (e) {
|
|
@@ -168,3 +187,21 @@ class SubscriptionConfigs {
|
|
|
168
187
|
});
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
/// Remote keys that drive the "update available" prompt. Set these in the
|
|
191
|
+
/// Firebase Remote Config console (works on every backend, since Firebase Core
|
|
192
|
+
/// is always initialized):
|
|
193
|
+
/// - `app_latest_version`: the newest version published to the stores. When the
|
|
194
|
+
/// installed version is older, an *optional* update sheet is shown.
|
|
195
|
+
/// - `app_min_version`: the oldest version still allowed. When the installed
|
|
196
|
+
/// version is older, a *forced* (blocking) update sheet is shown.
|
|
197
|
+
/// Both default to `0.0.0` (feature effectively off until you set real values).
|
|
198
|
+
class AppUpdateConfigs {
|
|
199
|
+
final RemoteConfigData<String> latestVersion;
|
|
200
|
+
final RemoteConfigData<String> minVersion;
|
|
201
|
+
|
|
202
|
+
AppUpdateConfigs({
|
|
203
|
+
required this.latestVersion,
|
|
204
|
+
required this.minVersion,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:go_router/go_router.dart';
|
|
3
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
3
4
|
|
|
4
5
|
enum RouterType {
|
|
5
6
|
goRouter,
|
|
@@ -30,18 +31,27 @@ class Guard extends StatelessWidget {
|
|
|
30
31
|
future: canActivate,
|
|
31
32
|
builder: (_, result) {
|
|
32
33
|
if (!result.hasData || result.hasError) {
|
|
33
|
-
return
|
|
34
|
+
return _loadingScreen(context);
|
|
34
35
|
}
|
|
35
36
|
final bool canActivate = result.data!;
|
|
36
37
|
if (canActivate) {
|
|
37
38
|
return child;
|
|
38
39
|
}
|
|
39
40
|
redirect(context);
|
|
40
|
-
return loading ??
|
|
41
|
+
return loading ?? _loadingScreen(context);
|
|
41
42
|
},
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/// Themed placeholder shown while the guard resolves or redirects. Never a
|
|
47
|
+
/// bare [Container] (which paints nothing and shows the black window behind).
|
|
48
|
+
Widget _loadingScreen(BuildContext context) {
|
|
49
|
+
return Material(
|
|
50
|
+
color: context.colors.background,
|
|
51
|
+
child: const Center(child: CircularProgressIndicator.adaptive()),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
void redirect(BuildContext context) {
|
|
46
56
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
47
57
|
if (!context.mounted) return;
|
|
@@ -64,5 +74,9 @@ class Guard extends StatelessWidget {
|
|
|
64
74
|
Navigator.of(context).pushReplacementNamed(fallbackRoute);
|
|
65
75
|
}
|
|
66
76
|
});
|
|
77
|
+
// addPostFrameCallback does not schedule a frame on its own; if the app is
|
|
78
|
+
// idle the redirect would otherwise wait until the next input event (the
|
|
79
|
+
// "stuck until you tap the screen" symptom). Force a frame so it runs now.
|
|
80
|
+
WidgetsBinding.instance.scheduleFrame();
|
|
67
81
|
}
|
|
68
82
|
}
|
|
@@ -50,6 +50,9 @@ abstract final class KasyIcons {
|
|
|
50
50
|
|
|
51
51
|
/// Collapse the left panel (sidebar) — narrow/expand toggle.
|
|
52
52
|
static const IconData panelLeft = LucideIcons.panelLeft300;
|
|
53
|
+
|
|
54
|
+
/// Hamburger — opens the navigation drawer (e.g. from an app-bar leading).
|
|
55
|
+
static const IconData menu = LucideIcons.menu300;
|
|
53
56
|
static const IconData cameraAlt = LucideIcons.camera400;
|
|
54
57
|
static const IconData check = LucideIcons.check300;
|
|
55
58
|
static const IconData checkCircle = LucideIcons.circleCheck300;
|
|
@@ -133,9 +133,7 @@ class RateBannerWidget extends StatelessWidget {
|
|
|
133
133
|
Text(
|
|
134
134
|
title,
|
|
135
135
|
textAlign: TextAlign.center,
|
|
136
|
-
style:
|
|
137
|
-
fontSize: 17,
|
|
138
|
-
fontWeight: FontWeight.w600,
|
|
136
|
+
style: context.kasyTextTheme.sectionTitle.copyWith(
|
|
139
137
|
color: cs.onSurface,
|
|
140
138
|
height: 1.3,
|
|
141
139
|
),
|
|
@@ -144,8 +142,7 @@ class RateBannerWidget extends StatelessWidget {
|
|
|
144
142
|
Text(
|
|
145
143
|
text,
|
|
146
144
|
textAlign: TextAlign.center,
|
|
147
|
-
style:
|
|
148
|
-
fontSize: 14,
|
|
145
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
149
146
|
color: cs.onSurface.withValues(alpha: 0.55),
|
|
150
147
|
height: 1.4,
|
|
151
148
|
),
|
|
@@ -21,9 +21,11 @@ Future<bool> showReviewDialog(
|
|
|
21
21
|
WidgetRef ref, {
|
|
22
22
|
bool force = false,
|
|
23
23
|
}) async {
|
|
24
|
-
// Store reviews are
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
// Store reviews are native-only (App Store / Play Store). In production the
|
|
25
|
+
// auto prompt never shows on web (nowhere to send the user). But a forced
|
|
26
|
+
// call — the admin preview — still shows the dialog on web so its design can
|
|
27
|
+
// be reviewed there; only the store action won't do anything.
|
|
28
|
+
if (kIsWeb && !force) {
|
|
27
29
|
return false;
|
|
28
30
|
}
|
|
29
31
|
if (!context.mounted) {
|
|
@@ -72,6 +72,20 @@ class SharedPreferencesBuilder implements OnStartService {
|
|
|
72
72
|
return prefs.getBool('biometric_enabled') ?? false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/// Whether the user has already been through the one-time onboarding intro.
|
|
76
|
+
///
|
|
77
|
+
/// Persisted locally so onboarding shows only once per install. Crucially it
|
|
78
|
+
/// survives logout (logout clears the account identity, not the fact that the
|
|
79
|
+
/// user has already seen the intro), so after signing out the user lands on
|
|
80
|
+
/// the sign-in screen instead of being dragged through onboarding again.
|
|
81
|
+
Future<void> setOnboardingCompleted(bool completed) async {
|
|
82
|
+
await prefs.setBool('onboarding_completed', completed);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bool getOnboardingCompleted() {
|
|
86
|
+
return prefs.getBool('onboarding_completed') ?? false;
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/// How many times the user dismissed the ATT soft prompt without accepting.
|
|
76
90
|
/// Used to back off and eventually stop asking. See [MaybeShowAttPermission].
|
|
77
91
|
int getAttSoftDismissCount() {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/core/app_update/app_update_repository.dart';
|
|
4
|
+
import 'package:kasy_kit/core/app_update/app_update_status.dart';
|
|
5
|
+
import 'package:kasy_kit/core/app_update/update_available_sheet.dart';
|
|
6
|
+
import 'package:kasy_kit/core/states/components/maybeshow_component.dart';
|
|
7
|
+
import 'package:kasy_kit/core/states/models/event_model.dart';
|
|
8
|
+
|
|
9
|
+
/// On app start, checks whether a newer version is available (remote config vs
|
|
10
|
+
/// installed version) and, if so, prompts the user to update via the store.
|
|
11
|
+
///
|
|
12
|
+
/// Native-only. Distinct from `MaybeShowUpdateBottomSheet` (the "what's new"
|
|
13
|
+
/// sheet, which runs *after* the user already updated): this runs *before*,
|
|
14
|
+
/// when the user is behind. It is registered first so a required update
|
|
15
|
+
/// pre-empts every other start-up prompt.
|
|
16
|
+
class MaybeShowUpdateAvailable extends MaybeShowWithRef {
|
|
17
|
+
@override
|
|
18
|
+
Future<bool> handle(WidgetRef ref, AppEvent event) async {
|
|
19
|
+
if (event is! OnAppStartEvent) return false;
|
|
20
|
+
if (kIsWeb) return false;
|
|
21
|
+
|
|
22
|
+
final status = await ref.read(appUpdateRepositoryProvider).check();
|
|
23
|
+
if (status == AppUpdateStatus.upToDate) return false;
|
|
24
|
+
if (!ref.context.mounted) return false;
|
|
25
|
+
|
|
26
|
+
await showUpdateAvailableSheet(
|
|
27
|
+
ref.context,
|
|
28
|
+
forced: status == AppUpdateStatus.forced,
|
|
29
|
+
);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -20,6 +20,10 @@ Future<void> confirmLogout(BuildContext context, WidgetRef ref) {
|
|
|
20
20
|
message: tr.disconnect_confirm_message,
|
|
21
21
|
cancelLabel: tr.disconnect_cancel,
|
|
22
22
|
confirmLabel: tr.disconnect,
|
|
23
|
-
|
|
23
|
+
// Async so the confirm button shows a spinner while signing out, then the
|
|
24
|
+
// router redirect lands the user on the sign-in screen. (Don't fire-and-
|
|
25
|
+
// forget: that closed the dialog instantly and left the old screen frozen.)
|
|
26
|
+
onConfirmAsync: () =>
|
|
27
|
+
ref.read(userStateNotifierProvider.notifier).onLogout(),
|
|
24
28
|
);
|
|
25
29
|
}
|
|
@@ -10,6 +10,7 @@ import 'package:kasy_kit/core/data/repositories/user_repository.dart';
|
|
|
10
10
|
import 'package:kasy_kit/core/initializer/onstart_service.dart';
|
|
11
11
|
import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
|
|
12
12
|
import 'package:kasy_kit/core/states/models/user_state.dart';
|
|
13
|
+
import 'package:kasy_kit/core/utils/image_bytes_loader.dart';
|
|
13
14
|
import 'package:kasy_kit/environments.dart';
|
|
14
15
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
15
16
|
import 'package:kasy_kit/features/notifications/providers/models/device.dart';
|
|
@@ -89,11 +90,18 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
89
90
|
state = const UserState(user: User.loading());
|
|
90
91
|
await _loadState();
|
|
91
92
|
await _initDeviceRegistration();
|
|
93
|
+
// A successful sign-in puts the user past first-run onboarding. Remember it
|
|
94
|
+
// so that after a later logout they land on the sign-in screen instead of
|
|
95
|
+
// being sent back through onboarding.
|
|
96
|
+
await ref.read(sharedPreferencesProvider).setOnboardingCompleted(true);
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/// Set the user as onboarded in the database
|
|
95
100
|
/// This function is called when the user has completed the onboarding
|
|
96
101
|
Future<void> onOnboarded() async {
|
|
102
|
+
// Remember locally that onboarding is done, so it's never shown again on
|
|
103
|
+
// this install (survives logout and account deletion).
|
|
104
|
+
await ref.read(sharedPreferencesProvider).setOnboardingCompleted(true);
|
|
97
105
|
if (state.user.idOrNull == null) {
|
|
98
106
|
// Guest with no account: persist onboarding in local state only.
|
|
99
107
|
// No Firestore document exists yet — no write needed.
|
|
@@ -104,10 +112,27 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
104
112
|
state = state.copyWith(user: newUser);
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
/// Finish onboarding (or "continue as guest" from the sign-in screen) by
|
|
116
|
+
/// making sure the user has a backend identity, then marking them onboarded.
|
|
117
|
+
///
|
|
118
|
+
/// This is the ONLY place an anonymous account is created. It is created
|
|
119
|
+
/// lazily here — never eagerly on app start, never on logout — so users who
|
|
120
|
+
/// never get this far (or who later sign out) don't leave orphan anonymous
|
|
121
|
+
/// accounts behind in the backend.
|
|
122
|
+
Future<void> continueAsGuest() async {
|
|
123
|
+
if (mode == AuthenticationMode.anonymous && state.user.idOrNull == null) {
|
|
124
|
+
await _loadAnonymousState();
|
|
125
|
+
}
|
|
126
|
+
await onOnboarded();
|
|
127
|
+
}
|
|
128
|
+
|
|
107
129
|
/// Mark the user as onboarded immediately (optimistic) and write to the
|
|
108
130
|
/// backend in background. Used by the skip-onboarding flow so navigation
|
|
109
131
|
/// to home is instant — no spinner while waiting for a network round-trip.
|
|
110
132
|
void onSkippedOnboarding() {
|
|
133
|
+
// Remember onboarding as done locally (same flag as [onOnboarded]) so it's
|
|
134
|
+
// never shown again, even after a logout.
|
|
135
|
+
unawaited(ref.read(sharedPreferencesProvider).setOnboardingCompleted(true));
|
|
111
136
|
state = state.copyWith(
|
|
112
137
|
user: switch (state.user) {
|
|
113
138
|
final AuthenticatedUserData u => u.copyWith(onboarded: true),
|
|
@@ -142,11 +167,14 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
142
167
|
await ref.read(sharedPreferencesProvider).setBiometricEnabled(false);
|
|
143
168
|
// Forget the last bottom-bar tab so the next login lands on the default tab
|
|
144
169
|
// (Home) instead of wherever the previous account left off.
|
|
145
|
-
|
|
170
|
+
forgetActiveTab();
|
|
146
171
|
state = const UserState(user: User.anonymous());
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
// No anonymous re-signup here. The router redirect sends the user to the
|
|
173
|
+
// sign-in screen (onboarding is remembered as done, so it isn't repeated).
|
|
174
|
+
// Recreating an anonymous account on every logout would pile up orphan
|
|
175
|
+
// users in the backend — an anonymous account is only ever created lazily,
|
|
176
|
+
// in [continueAsGuest], when the user finishes onboarding or explicitly
|
|
177
|
+
// chooses to continue as a guest.
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
/// Refresh the user
|
|
@@ -164,6 +192,41 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
164
192
|
state = state.copyWith(user: user ?? User.anonymous(id: id));
|
|
165
193
|
}
|
|
166
194
|
|
|
195
|
+
/// On first social sign-in, copy the provider's profile photo (e.g. Google)
|
|
196
|
+
/// into our own storage so the user starts with an avatar. Best-effort and
|
|
197
|
+
/// one-shot: only runs when the user has no avatar yet, and it never overwrites
|
|
198
|
+
/// a photo the user set manually. Apple does not expose a photo (no-op there).
|
|
199
|
+
Future<void> _importSocialAvatarIfNeeded() async {
|
|
200
|
+
final user = state.user;
|
|
201
|
+
if (user is! AuthenticatedUserData) return;
|
|
202
|
+
final userId = user.id;
|
|
203
|
+
if (userId == null || userId.isEmpty) return;
|
|
204
|
+
if (user.avatarPath?.isNotEmpty ?? false) return; // keep existing avatar
|
|
205
|
+
try {
|
|
206
|
+
final photoUrl = await _authenticationRepository.getCurrentUserPhotoUrl();
|
|
207
|
+
if (photoUrl == null || photoUrl.isEmpty) return;
|
|
208
|
+
final bytes = await loadImageBytes(_normalizeAvatarUrl(photoUrl));
|
|
209
|
+
if (bytes == null || bytes.isEmpty) return;
|
|
210
|
+
await for (final _ in _userRepository.saveAvatar(
|
|
211
|
+
userId: userId,
|
|
212
|
+
data: Uint8List.fromList(bytes),
|
|
213
|
+
)) {}
|
|
214
|
+
await refresh();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
// Best-effort: keep the fallback avatar if the import fails.
|
|
217
|
+
_logger.w('Social avatar import skipped: $e');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Google serves profile photos with a size suffix (e.g. `=s96-c`, only 96px).
|
|
222
|
+
/// Request a ~400px source so the avatar stays crisp after our 450px
|
|
223
|
+
/// re-compression. URLs from other providers are returned unchanged.
|
|
224
|
+
String _normalizeAvatarUrl(String url) {
|
|
225
|
+
if (!url.contains('googleusercontent.com')) return url;
|
|
226
|
+
final base = url.contains('=') ? url.substring(0, url.indexOf('=')) : url;
|
|
227
|
+
return '$base=s400-c';
|
|
228
|
+
}
|
|
229
|
+
|
|
167
230
|
/// This function is called after a user successfuly purchased a subscription
|
|
168
231
|
/// It will refresh the subscription state without waiting for the webhook
|
|
169
232
|
/// (which can take some time and could show a wrong state to the user)
|
|
@@ -208,11 +271,14 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
208
271
|
}
|
|
209
272
|
await _userRepository.delete();
|
|
210
273
|
await _authenticationRepository.logout();
|
|
274
|
+
// Same as onLogout: forget the last bottom-bar tab so the next account that
|
|
275
|
+
// signs in lands on Home, not wherever the deleted account left off (the
|
|
276
|
+
// user deletes from Settings, so without this the next login reopens it).
|
|
277
|
+
forgetActiveTab();
|
|
211
278
|
state = const UserState(user: User.anonymous());
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
279
|
+
// Same as [onLogout]: no anonymous re-signup. The user lands on the sign-in
|
|
280
|
+
// screen; a new anonymous account is only created lazily if they choose to
|
|
281
|
+
// continue as a guest.
|
|
216
282
|
}
|
|
217
283
|
|
|
218
284
|
// -------------------------------
|
|
@@ -252,13 +318,16 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
252
318
|
Future<void> _loadState() async {
|
|
253
319
|
final credentials = await _authenticationRepository.get();
|
|
254
320
|
|
|
255
|
-
if (credentials == null
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
321
|
+
if (credentials == null) {
|
|
322
|
+
// No account yet. We deliberately do NOT create an anonymous account here
|
|
323
|
+
// anymore (even in anonymous mode). It's created lazily in
|
|
324
|
+
// [continueAsGuest] when the user finishes onboarding or taps "continue
|
|
325
|
+
// as guest", so simply opening the app — or signing out — never piles up
|
|
326
|
+
// orphan anonymous users. The router redirect decides where to send them:
|
|
327
|
+
// onboarding on the very first run, the sign-in screen after a logout.
|
|
328
|
+
_logger.i('No credentials: user starts as a guest with no account yet');
|
|
260
329
|
state = state.copyWith(user: const User.anonymous());
|
|
261
|
-
} else
|
|
330
|
+
} else {
|
|
262
331
|
_logger.i('User is connected with id ${credentials.id}');
|
|
263
332
|
var user = await _userRepository.get(credentials.id);
|
|
264
333
|
// Retry a few times: the Cloud Function onUserRegistration may not have
|
|
@@ -299,10 +368,12 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
299
368
|
);
|
|
300
369
|
final syncedUser = await _userRepository.get(credentials.id);
|
|
301
370
|
state = state.copyWith(user: syncedUser ?? user);
|
|
371
|
+
unawaited(_importSocialAvatarIfNeeded());
|
|
302
372
|
return;
|
|
303
373
|
}
|
|
304
374
|
}
|
|
305
375
|
state = state.copyWith(user: user);
|
|
376
|
+
unawaited(_importSocialAvatarIfNeeded());
|
|
306
377
|
}
|
|
307
378
|
}
|
|
308
379
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/theme/texts.dart';
|
|
3
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
4
|
+
|
|
5
|
+
/// Republishes the app's typography for the current breakpoint — without
|
|
6
|
+
/// touching a single call site.
|
|
7
|
+
///
|
|
8
|
+
/// It reads the effective viewport width, picks the [DeviceType], and rebuilds
|
|
9
|
+
/// the Material `textTheme` and the [KasyTextTheme] extension from the
|
|
10
|
+
/// per-breakpoint [KasyTypeScale] ramp. Headings step down from desktop to
|
|
11
|
+
/// mobile; body and labels stay constant. Colours are kept from the base theme
|
|
12
|
+
/// (the ramp is colourless), so every `context.textTheme.*` and
|
|
13
|
+
/// `context.kasyTextTheme.*` below inherits the breakpoint's sizes.
|
|
14
|
+
///
|
|
15
|
+
/// Placed as the innermost wrapper of the app (inside `WebViewportScale` and
|
|
16
|
+
/// `DevicePreview`), so it reads the *same* width the screens lay out against:
|
|
17
|
+
/// the type bucket always matches the layout bucket. Desktop is the authored
|
|
18
|
+
/// baseline, so it is a pass-through there.
|
|
19
|
+
class ResponsiveTextTheme extends StatelessWidget {
|
|
20
|
+
final Widget child;
|
|
21
|
+
|
|
22
|
+
const ResponsiveTextTheme({super.key, required this.child});
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
Widget build(BuildContext context) {
|
|
26
|
+
final DeviceType device =
|
|
27
|
+
DeviceType.fromWidth(MediaQuery.sizeOf(context).width);
|
|
28
|
+
|
|
29
|
+
// Desktop is the authored baseline (the base theme built in main.dart) — no
|
|
30
|
+
// rebuild needed.
|
|
31
|
+
if (device != DeviceType.small && device != DeviceType.medium) {
|
|
32
|
+
return child;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
final ThemeData theme = Theme.of(context);
|
|
36
|
+
final KasyTextTheme ramp = KasyTextTheme.build(device);
|
|
37
|
+
final TextTheme base = theme.textTheme;
|
|
38
|
+
|
|
39
|
+
// Take size/weight/line-height from the ramp, keep colour from the base
|
|
40
|
+
// (the ramp is colourless, so merge preserves the base slot's colour).
|
|
41
|
+
final TextTheme rebuilt = TextTheme(
|
|
42
|
+
displayLarge: base.displayLarge?.merge(ramp.displayLarge),
|
|
43
|
+
displayMedium: base.displayMedium?.merge(ramp.displayMedium),
|
|
44
|
+
displaySmall: base.displaySmall?.merge(ramp.displaySmall),
|
|
45
|
+
headlineLarge: base.headlineLarge?.merge(ramp.headlineLarge),
|
|
46
|
+
headlineMedium: base.headlineMedium?.merge(ramp.headlineMedium),
|
|
47
|
+
headlineSmall: base.headlineSmall?.merge(ramp.headlineSmall),
|
|
48
|
+
titleLarge: base.titleLarge?.merge(ramp.titleLarge),
|
|
49
|
+
titleMedium: base.titleMedium?.merge(ramp.titleMedium),
|
|
50
|
+
titleSmall: base.titleSmall?.merge(ramp.titleSmall),
|
|
51
|
+
bodyLarge: base.bodyLarge?.merge(ramp.bodyLarge),
|
|
52
|
+
bodyMedium: base.bodyMedium?.merge(ramp.bodyMedium),
|
|
53
|
+
bodySmall: base.bodySmall?.merge(ramp.bodySmall),
|
|
54
|
+
labelLarge: base.labelLarge?.merge(ramp.labelLarge),
|
|
55
|
+
labelMedium: base.labelMedium?.merge(ramp.labelMedium),
|
|
56
|
+
labelSmall: base.labelSmall?.merge(ramp.labelSmall),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Replace only the KasyTextTheme extension; keep every other extension.
|
|
60
|
+
final List<ThemeExtension<dynamic>> extensions = theme.extensions.values
|
|
61
|
+
.map((e) => e is KasyTextTheme ? ramp : e)
|
|
62
|
+
.toList();
|
|
63
|
+
|
|
64
|
+
return Theme(
|
|
65
|
+
data: theme.copyWith(textTheme: rebuilt, extensions: extensions),
|
|
66
|
+
child: child,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:google_fonts/google_fonts.dart';
|
|
3
|
+
import 'package:kasy_kit/core/theme/type_scale.dart';
|
|
4
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
3
5
|
|
|
4
6
|
/// Kasy Design System — Typography Tokens (HeroUI type scale)
|
|
5
7
|
///
|
|
@@ -32,6 +34,22 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
32
34
|
decoration: decoration,
|
|
33
35
|
);
|
|
34
36
|
|
|
37
|
+
// Builds a role from its responsive [RampSize] at a given [device]: the size
|
|
38
|
+
// (and proportional line-height) come from the breakpoint, the weight from
|
|
39
|
+
// the role. This is how every slot picks up the per-breakpoint type scale.
|
|
40
|
+
static TextStyle _fromRamp(
|
|
41
|
+
FontWeight weight,
|
|
42
|
+
RampSize ramp,
|
|
43
|
+
DeviceType device, {
|
|
44
|
+
TextDecoration? decoration,
|
|
45
|
+
}) =>
|
|
46
|
+
_inter(
|
|
47
|
+
weight,
|
|
48
|
+
ramp.size(device),
|
|
49
|
+
ramp.lineHeight(device),
|
|
50
|
+
decoration: decoration,
|
|
51
|
+
);
|
|
52
|
+
|
|
35
53
|
// -----------------------------------------------------------------------
|
|
36
54
|
// HeroUI roles — canonical styles (color applied per-theme downstream)
|
|
37
55
|
// -----------------------------------------------------------------------
|
|
@@ -54,37 +72,9 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
54
72
|
static TextStyle get buttonBase => _inter(FontWeight.w500, 16, 24);
|
|
55
73
|
static TextStyle get buttonSm => _inter(FontWeight.w500, 14, 20);
|
|
56
74
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// scale role above; restyle a role app-wide by editing it here once.
|
|
61
|
-
// Apply color at the call site (.copyWith(color: ...)); the scale is colorless.
|
|
62
|
-
// -----------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
65
|
-
static TextStyle get pageTitle => heading2;
|
|
66
|
-
|
|
67
|
-
/// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
|
|
68
|
-
static TextStyle get sectionTitle => heading4;
|
|
69
|
-
|
|
70
|
-
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
71
|
-
static TextStyle get sectionLabel =>
|
|
72
|
-
_inter(FontWeight.w600, 12, 16).copyWith(letterSpacing: 0.5);
|
|
73
|
-
|
|
74
|
-
/// Primary text of a list / settings row. 14 / w500.
|
|
75
|
-
static TextStyle get rowTitle => bodySmMedium;
|
|
76
|
-
|
|
77
|
-
/// Secondary / value text in a row (apply a muted color at the call site). 14 / w400.
|
|
78
|
-
static TextStyle get rowValue => bodySm;
|
|
79
|
-
|
|
80
|
-
/// Card title. 14 / w500.
|
|
81
|
-
static TextStyle get cardTitle => bodySmMedium;
|
|
82
|
-
|
|
83
|
-
/// Card subtitle / supporting line. 12 / w400.
|
|
84
|
-
static TextStyle get cardSubtitle => bodyXs;
|
|
85
|
-
|
|
86
|
-
/// Caption, hint, version label, footnote. 12 / w400.
|
|
87
|
-
static TextStyle get caption => bodyXs;
|
|
75
|
+
// Semantic UI roles (pageTitle, sectionTitle, rowTitle, …) live as INSTANCE
|
|
76
|
+
// getters further down, so they pick up the per-breakpoint scaling. The raw
|
|
77
|
+
// scale roles above stay static (the authored spec / design-system baseline).
|
|
88
78
|
|
|
89
79
|
// -----------------------------------------------------------------------
|
|
90
80
|
// Display — large hero text (extends the HeroUI scale upward in Inter)
|
|
@@ -143,35 +133,78 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
143
133
|
required this.primary,
|
|
144
134
|
});
|
|
145
135
|
|
|
146
|
-
|
|
136
|
+
// -----------------------------------------------------------------------
|
|
137
|
+
// Semantic app roles — INSTANCE getters (responsive).
|
|
138
|
+
//
|
|
139
|
+
// These mirror the static roles above, but derive from this instance's
|
|
140
|
+
// fields, so they pick up the per-breakpoint scaling applied by
|
|
141
|
+
// [ResponsiveTextTheme]. Prefer `context.kasyTextTheme.<role>` on screens
|
|
142
|
+
// when you need a role that scales (the static `KasyTextTheme.<role>` stays
|
|
143
|
+
// fixed at the desktop baseline — fine for specs/docs, not for live UI).
|
|
144
|
+
// Apply colour at the call site; the scale is colourless.
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
148
|
+
TextStyle get pageTitle => headlineMedium;
|
|
149
|
+
|
|
150
|
+
/// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
|
|
151
|
+
TextStyle get sectionTitle => titleMedium;
|
|
152
|
+
|
|
153
|
+
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
154
|
+
TextStyle get sectionLabel =>
|
|
155
|
+
bodySmall.copyWith(fontWeight: FontWeight.w600, letterSpacing: 0.5);
|
|
156
|
+
|
|
157
|
+
/// Primary text of a list / settings row. 14 / w500.
|
|
158
|
+
TextStyle get rowTitle => titleSmall;
|
|
159
|
+
|
|
160
|
+
/// Secondary / value text in a row (apply a muted colour at the call site). 14 / w400.
|
|
161
|
+
TextStyle get rowValue => bodyMedium;
|
|
162
|
+
|
|
163
|
+
/// Card title. 14 / w500.
|
|
164
|
+
TextStyle get cardTitle => titleSmall;
|
|
165
|
+
|
|
166
|
+
/// Card subtitle / supporting line. 12 / w400.
|
|
167
|
+
TextStyle get cardSubtitle => bodySmall;
|
|
168
|
+
|
|
169
|
+
/// Caption, hint, version label, footnote. 12 / w400.
|
|
170
|
+
TextStyle get caption => bodySmall;
|
|
171
|
+
|
|
172
|
+
/// Builds the full text theme for a given [device].
|
|
173
|
+
///
|
|
174
|
+
/// Each slot resolves its size from the responsive [KasyTypeScale] ramp at the
|
|
175
|
+
/// breakpoint: headings step down from desktop to mobile, body and labels stay
|
|
176
|
+
/// constant. Defaults to desktop ([DeviceType.large]) so the base theme built
|
|
177
|
+
/// once in `main.dart` is the authored reference; [ResponsiveTextTheme]
|
|
178
|
+
/// rebuilds it per breakpoint below the MediaQuery.
|
|
179
|
+
factory KasyTextTheme.build([DeviceType device = DeviceType.large]) {
|
|
147
180
|
return KasyTextTheme(
|
|
148
|
-
// Display — hero text
|
|
149
|
-
displayLarge:
|
|
150
|
-
displayMedium:
|
|
151
|
-
displaySmall: heading1,
|
|
152
|
-
|
|
153
|
-
// Headline →
|
|
154
|
-
headlineLarge: heading1,
|
|
155
|
-
headlineMedium: heading2,
|
|
156
|
-
headlineSmall: heading3,
|
|
157
|
-
|
|
158
|
-
// Title →
|
|
159
|
-
titleLarge: heading3,
|
|
160
|
-
titleMedium: heading4,
|
|
161
|
-
titleSmall:
|
|
162
|
-
|
|
163
|
-
// Body →
|
|
164
|
-
bodyLarge: bodyBase,
|
|
165
|
-
bodyMedium: bodySm,
|
|
166
|
-
bodySmall: bodyXs,
|
|
167
|
-
|
|
168
|
-
// Label →
|
|
169
|
-
labelLarge:
|
|
170
|
-
labelMedium:
|
|
171
|
-
labelSmall:
|
|
181
|
+
// Display — hero text.
|
|
182
|
+
displayLarge: _fromRamp(FontWeight.w800, KasyTypeScale.displayLarge, device),
|
|
183
|
+
displayMedium: _fromRamp(FontWeight.w800, KasyTypeScale.displayMedium, device),
|
|
184
|
+
displaySmall: _fromRamp(FontWeight.w800, KasyTypeScale.heading1, device),
|
|
185
|
+
|
|
186
|
+
// Headline → Heading 1–3.
|
|
187
|
+
headlineLarge: _fromRamp(FontWeight.w800, KasyTypeScale.heading1, device),
|
|
188
|
+
headlineMedium: _fromRamp(FontWeight.w700, KasyTypeScale.heading2, device),
|
|
189
|
+
headlineSmall: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
190
|
+
|
|
191
|
+
// Title → Heading 3 / Heading 4 / Body sm medium.
|
|
192
|
+
titleLarge: _fromRamp(FontWeight.w600, KasyTypeScale.heading3, device),
|
|
193
|
+
titleMedium: _fromRamp(FontWeight.w600, KasyTypeScale.heading4, device),
|
|
194
|
+
titleSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
195
|
+
|
|
196
|
+
// Body → Body base / Body sm / Body xs.
|
|
197
|
+
bodyLarge: _fromRamp(FontWeight.w400, KasyTypeScale.bodyBase, device),
|
|
198
|
+
bodyMedium: _fromRamp(FontWeight.w400, KasyTypeScale.bodySm, device),
|
|
199
|
+
bodySmall: _fromRamp(FontWeight.w400, KasyTypeScale.bodyXs, device),
|
|
200
|
+
|
|
201
|
+
// Label → Button sm / Body xs medium.
|
|
202
|
+
labelLarge: _fromRamp(FontWeight.w500, KasyTypeScale.bodySm, device),
|
|
203
|
+
labelMedium: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
204
|
+
labelSmall: _fromRamp(FontWeight.w500, KasyTypeScale.bodyXs, device),
|
|
172
205
|
|
|
173
206
|
// Legacy primary.
|
|
174
|
-
primary: bodyBase,
|
|
207
|
+
primary: _fromRamp(FontWeight.w400, KasyTypeScale.bodyBase, device),
|
|
175
208
|
);
|
|
176
209
|
}
|
|
177
210
|
|