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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
|
+
import 'package:universal_html/html.dart' as html;
|
|
3
|
+
|
|
4
|
+
/// Forces the browser address bar to [path] on web (no-op on native).
|
|
5
|
+
///
|
|
6
|
+
/// Why this exists: the bottom bar (Bart) writes each tab's URL directly via
|
|
7
|
+
/// `history.pushState`, bypassing GoRouter — and it never writes the Home tab's
|
|
8
|
+
/// URL (Bart short-circuits when the tab index doesn't change, and Home is the
|
|
9
|
+
/// default index). So after a fresh login forces Home, GoRouter is at `/` but
|
|
10
|
+
/// the address bar still shows the previous session's tab (e.g. `/settings`),
|
|
11
|
+
/// and `go('/')` is a no-op because GoRouter already considers itself at `/`.
|
|
12
|
+
///
|
|
13
|
+
/// `replaceState` (not `pushState`) so the stale entry is corrected in place,
|
|
14
|
+
/// without adding a bogus history step the user could "back" into.
|
|
15
|
+
void syncBrowserUrl(String path) {
|
|
16
|
+
if (!kIsWeb) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
html.window.history.replaceState(null, '', path);
|
|
20
|
+
}
|
|
@@ -71,6 +71,28 @@ class KasyChromeVisibility {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/// Brings the chrome back whenever a route is pushed onto or popped from the
|
|
75
|
+
/// root navigator. Tab switches already reset via [BartScaffold.onRouteChanged],
|
|
76
|
+
/// but detail screens (feedback, reminders, …) are pushed on the root navigator
|
|
77
|
+
/// and bypass that — so without this, returning to a screen that had scrolled
|
|
78
|
+
/// its chrome away would leave the app bar and bottom menu stuck hidden.
|
|
79
|
+
///
|
|
80
|
+
/// Only the chrome is restored; the destination screen keeps its scroll position
|
|
81
|
+
/// (the framework preserves it), matching how large apps handle back navigation.
|
|
82
|
+
class KasyChromeVisibilityObserver extends NavigatorObserver {
|
|
83
|
+
void _reset() => KasyChromeVisibility.instance.resetShown();
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
|
|
93
|
+
_reset();
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
// ---------------------------------------------------------------------------
|
|
75
97
|
// Configuration — edit these to change the experience your app ships with.
|
|
76
98
|
// ---------------------------------------------------------------------------
|
|
@@ -12,6 +12,11 @@ const bool withLocalReminders = true;
|
|
|
12
12
|
/// the CLI generates them as `false`. Apple shows on iOS/macOS regardless; it is
|
|
13
13
|
/// hidden on Android (also needs a paid Service ID, and the native flow throws).
|
|
14
14
|
const bool withAppleWebSignin = true;
|
|
15
|
+
/// When true, the Facebook sign-in button is shown on WEB. Facebook-on-web works
|
|
16
|
+
/// on the Firebase backend (signInWithPopup) after `kasy facebook`; on Supabase the
|
|
17
|
+
/// web flow isn't wired yet (roadmap), so the CLI ships this `false` there. Native
|
|
18
|
+
/// (iOS/Android) always shows the Facebook button regardless of this flag.
|
|
19
|
+
const bool withFacebookWebSignin = false;
|
|
15
20
|
/// When true, the app includes web support:
|
|
16
21
|
/// - anonymous sign-up is disabled on web (user is redirected to /signin)
|
|
17
22
|
/// - onboarding is skipped on web
|
|
@@ -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
|
),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
1
2
|
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter_animate/flutter_animate.dart';
|
|
3
3
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
4
|
import 'package:go_router/go_router.dart';
|
|
5
5
|
import 'package:kasy_kit/components/components.dart';
|
|
@@ -7,17 +7,27 @@ import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
|
7
7
|
import 'package:kasy_kit/core/rating/providers/rating_repository.dart';
|
|
8
8
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
9
9
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
|
-
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
11
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
11
|
import 'package:logger/logger.dart';
|
|
13
12
|
|
|
14
13
|
/// Shows the in-app review dialog. Prefer a stable [context] (not a sheet that
|
|
15
14
|
/// will be popped before the async work finishes).
|
|
15
|
+
///
|
|
16
|
+
/// A clean [KasyDialog]: a title, a short message and two stacked actions
|
|
17
|
+
/// (write a review / suggest improvements). Dismissing via the dialog's own
|
|
18
|
+
/// close button just defers the next ask and keeps the user where they are.
|
|
16
19
|
Future<bool> showReviewDialog(
|
|
17
20
|
BuildContext context,
|
|
18
21
|
WidgetRef ref, {
|
|
19
22
|
bool force = false,
|
|
20
23
|
}) async {
|
|
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) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
21
31
|
if (!context.mounted) {
|
|
22
32
|
return false;
|
|
23
33
|
}
|
|
@@ -41,97 +51,42 @@ Future<bool> showReviewDialog(
|
|
|
41
51
|
barrierDismissible: false,
|
|
42
52
|
builder: (dialogContext) {
|
|
43
53
|
ratingRepository.delay();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'assets/images/review.png',
|
|
81
|
-
fit: BoxFit.fitWidth,
|
|
82
|
-
width: maxWidth,
|
|
83
|
-
),
|
|
84
|
-
),
|
|
85
|
-
Positioned(
|
|
86
|
-
top: KasySpacing.sm,
|
|
87
|
-
left: KasySpacing.sm,
|
|
88
|
-
child: CloseIcon(
|
|
89
|
-
onExit: () {
|
|
90
|
-
analytics.logEvent('rating_popup_close', {});
|
|
91
|
-
rating.delay().then((_) {
|
|
92
|
-
if (!dialogContext.mounted) return;
|
|
93
|
-
Navigator.of(dialogContext).pop();
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
),
|
|
97
|
-
),
|
|
98
|
-
],
|
|
99
|
-
),
|
|
100
|
-
const SizedBox(height: KasySpacing.md),
|
|
101
|
-
Text(
|
|
102
|
-
translations.description,
|
|
103
|
-
textAlign: TextAlign.center,
|
|
104
|
-
style: Theme.of(dialogContext).textTheme.bodyMedium,
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
),
|
|
108
|
-
footer: Column(
|
|
109
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
110
|
-
children: [
|
|
111
|
-
KasyButton(
|
|
112
|
-
label: translations.rate_button,
|
|
113
|
-
expand: true,
|
|
114
|
-
onPressed: () {
|
|
115
|
-
analytics.logEvent('rating_popup_show', {});
|
|
116
|
-
ratingRepository.rate().then((_) => rating.review()).then(
|
|
117
|
-
(_) {
|
|
118
|
-
if (!dialogContext.mounted) return;
|
|
119
|
-
Navigator.of(dialogContext).pop();
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
),
|
|
124
|
-
const SizedBox(height: KasySpacing.sm),
|
|
125
|
-
KasyButton(
|
|
126
|
-
label: translations.cancel_button,
|
|
127
|
-
variant: KasyButtonVariant.soft,
|
|
128
|
-
expand: true,
|
|
129
|
-
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
130
|
-
),
|
|
131
|
-
],
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
},
|
|
54
|
+
final translations = Translations.of(dialogContext).review_popup;
|
|
55
|
+
return KasyDialog(
|
|
56
|
+
leadingIcon: KasyIcons.star,
|
|
57
|
+
iconTone: KasyDialogIconTone.info,
|
|
58
|
+
title: translations.title,
|
|
59
|
+
titleCentered: true,
|
|
60
|
+
message: translations.description,
|
|
61
|
+
onClose: () {
|
|
62
|
+
analytics.logEvent('rating_popup_close', {});
|
|
63
|
+
rating.delay();
|
|
64
|
+
Navigator.of(dialogContext).pop();
|
|
65
|
+
},
|
|
66
|
+
footer: Column(
|
|
67
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
68
|
+
children: [
|
|
69
|
+
KasyButton(
|
|
70
|
+
label: translations.rate_button,
|
|
71
|
+
expand: true,
|
|
72
|
+
onPressed: () {
|
|
73
|
+
analytics.logEvent('rating_popup_show', {});
|
|
74
|
+
ratingRepository.rate().then((_) => rating.review()).then(
|
|
75
|
+
(_) {
|
|
76
|
+
if (!dialogContext.mounted) return;
|
|
77
|
+
Navigator.of(dialogContext).pop();
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
const SizedBox(height: KasySpacing.sm),
|
|
83
|
+
KasyButton(
|
|
84
|
+
label: translations.cancel_button,
|
|
85
|
+
variant: KasyButtonVariant.soft,
|
|
86
|
+
expand: true,
|
|
87
|
+
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
88
|
+
),
|
|
89
|
+
],
|
|
135
90
|
),
|
|
136
91
|
);
|
|
137
92
|
},
|
|
@@ -142,34 +97,3 @@ Future<bool> showReviewDialog(
|
|
|
142
97
|
}
|
|
143
98
|
return true;
|
|
144
99
|
}
|
|
145
|
-
|
|
146
|
-
class CloseIcon extends StatelessWidget {
|
|
147
|
-
final VoidCallback onExit;
|
|
148
|
-
|
|
149
|
-
const CloseIcon({super.key, required this.onExit});
|
|
150
|
-
|
|
151
|
-
@override
|
|
152
|
-
Widget build(BuildContext context) {
|
|
153
|
-
return ClipOval(
|
|
154
|
-
child: Material(
|
|
155
|
-
color: Colors.transparent,
|
|
156
|
-
child: InkWell(
|
|
157
|
-
onTap: () => onExit.call(),
|
|
158
|
-
child: Ink(
|
|
159
|
-
width: 32,
|
|
160
|
-
height: 32,
|
|
161
|
-
decoration: BoxDecoration(
|
|
162
|
-
color: context.colors.background,
|
|
163
|
-
shape: BoxShape.circle,
|
|
164
|
-
),
|
|
165
|
-
child: Icon(
|
|
166
|
-
KasyIcons.close,
|
|
167
|
-
color: context.colors.onBackground,
|
|
168
|
-
size: KasyIconSize.lg,
|
|
169
|
-
),
|
|
170
|
-
),
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -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
|
|