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
|
@@ -7,7 +7,10 @@ import 'package:flutter/services.dart';
|
|
|
7
7
|
import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
|
8
8
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
9
9
|
import 'package:google_sign_in/google_sign_in.dart';
|
|
10
|
+
import 'package:kasy_kit/core/config/features.dart';
|
|
10
11
|
import 'package:kasy_kit/core/data/entities/user_entity.dart';
|
|
12
|
+
import 'package:kasy_kit/features/authentication/api/auth_web_support.dart'
|
|
13
|
+
if (dart.library.js_interop) 'package:kasy_kit/features/authentication/api/auth_web_support_web.dart';
|
|
11
14
|
import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
|
|
12
15
|
import 'package:kasy_kit/features/authentication/api/popup_dismiss_watcher.dart'
|
|
13
16
|
if (dart.library.js_interop) 'package:kasy_kit/features/authentication/api/popup_dismiss_watcher_web.dart';
|
|
@@ -48,6 +51,23 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
48
51
|
}
|
|
49
52
|
_user = event;
|
|
50
53
|
});
|
|
54
|
+
if (kIsWeb) {
|
|
55
|
+
// Complete a pending mobile-web redirect sign-in (see signinWithGoogle /
|
|
56
|
+
// signinWithApple / signinWithFacebook). On desktop, or when no redirect
|
|
57
|
+
// is pending, this resolves immediately and is a no-op.
|
|
58
|
+
try {
|
|
59
|
+
await _auth.getRedirectResult();
|
|
60
|
+
} on FirebaseAuthException catch (e) {
|
|
61
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
62
|
+
Logger().w(
|
|
63
|
+
'Redirect sign-in: an account already exists with this email '
|
|
64
|
+
'under a different provider.',
|
|
65
|
+
);
|
|
66
|
+
} else if (!_isUserCancelledPopup(e.code)) {
|
|
67
|
+
Logger().w('Redirect sign-in failed: ${e.code}');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
51
71
|
var attempts = 0;
|
|
52
72
|
while (!hasInit && attempts < 20) {
|
|
53
73
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
@@ -177,6 +197,38 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
177
197
|
}
|
|
178
198
|
}
|
|
179
199
|
|
|
200
|
+
/// Mobile-web OAuth: start a full-page redirect to the provider instead of a
|
|
201
|
+
/// popup. Mobile browsers handle popups unreliably (the popup result is often
|
|
202
|
+
/// lost when the browser reclaims the backgrounded opener tab), leaving the app
|
|
203
|
+
/// stuck "signing in". The redirect result is picked up at startup by
|
|
204
|
+
/// [getRedirectResult] in [init]. The page is navigating away as soon as
|
|
205
|
+
/// [signInWithRedirect] resolves, so this future intentionally never completes
|
|
206
|
+
/// — the caller stays in its loading state until the browser leaves.
|
|
207
|
+
/// See [isMobileWebBrowser]. When a Firebase anonymous user exists we link the
|
|
208
|
+
/// provider to it (preserving the UID, same as the popup path does); otherwise
|
|
209
|
+
/// we sign in. The linked/signed-in user is materialised at startup by
|
|
210
|
+
/// [getRedirectResult].
|
|
211
|
+
Future<Credentials> _signInWithRedirectWeb(AuthProvider provider) async {
|
|
212
|
+
final current = _auth.currentUser;
|
|
213
|
+
if (current != null && current.isAnonymous) {
|
|
214
|
+
await current.linkWithRedirect(provider);
|
|
215
|
+
} else {
|
|
216
|
+
await _auth.signInWithRedirect(provider);
|
|
217
|
+
}
|
|
218
|
+
return Completer<Credentials>().future;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// Mobile-web account linking (Settings): link [provider] to [user] via a
|
|
222
|
+
/// full-page redirect instead of a popup, for the same reason as
|
|
223
|
+
/// [_signInWithRedirectWeb]. The link is completed at startup by
|
|
224
|
+
/// [getRedirectResult] in [init] (it reconnects to the persisted user). The
|
|
225
|
+
/// page navigates away as [linkWithRedirect] resolves, so this future never
|
|
226
|
+
/// completes — the caller stays in its loading state until the browser leaves.
|
|
227
|
+
Future<void> _linkWithRedirectWeb(User user, AuthProvider provider) async {
|
|
228
|
+
await user.linkWithRedirect(provider);
|
|
229
|
+
return Completer<void>().future;
|
|
230
|
+
}
|
|
231
|
+
|
|
180
232
|
@override
|
|
181
233
|
Future<Credentials> signinWithGoogle() async {
|
|
182
234
|
if (kIsWeb) {
|
|
@@ -188,6 +240,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
188
240
|
// googleProvider.setCustomParameters({
|
|
189
241
|
// 'login_hint': 'user@example.com'
|
|
190
242
|
// });
|
|
243
|
+
if (isMobileWebBrowser()) {
|
|
244
|
+
return _signInWithRedirectWeb(googleProvider);
|
|
245
|
+
}
|
|
191
246
|
try {
|
|
192
247
|
final credentials = await _popupOrCancel(
|
|
193
248
|
() => _auth.signInWithPopup(googleProvider),
|
|
@@ -200,6 +255,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
200
255
|
if (_isUserCancelledPopup(e.code)) {
|
|
201
256
|
throw const UserCancelledSignInException();
|
|
202
257
|
}
|
|
258
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
259
|
+
throw const EmailAlreadyRegisteredException();
|
|
260
|
+
}
|
|
203
261
|
rethrow;
|
|
204
262
|
}
|
|
205
263
|
}
|
|
@@ -232,6 +290,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
232
290
|
Future<Credentials> signupFromAnonymousWithGoogle() async {
|
|
233
291
|
if (kIsWeb) {
|
|
234
292
|
final googleProvider = GoogleAuthProvider();
|
|
293
|
+
if (isMobileWebBrowser()) {
|
|
294
|
+
return _signInWithRedirectWeb(googleProvider);
|
|
295
|
+
}
|
|
235
296
|
try {
|
|
236
297
|
final userCredential = await _popupOrCancel(
|
|
237
298
|
() => _auth.currentUser != null
|
|
@@ -246,6 +307,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
246
307
|
if (_isUserCancelledPopup(e.code)) {
|
|
247
308
|
throw const UserCancelledSignInException();
|
|
248
309
|
}
|
|
310
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
311
|
+
throw const EmailAlreadyRegisteredException();
|
|
312
|
+
}
|
|
249
313
|
if (e.code == 'credential-already-in-use') {
|
|
250
314
|
// Delete orphaned anonymous user before signing in with existing account.
|
|
251
315
|
final anonymousUser = _auth.currentUser;
|
|
@@ -285,6 +349,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
285
349
|
if (_isUserCancelledPopup(e.code)) {
|
|
286
350
|
throw const UserCancelledSignInException();
|
|
287
351
|
}
|
|
352
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
353
|
+
throw const EmailAlreadyRegisteredException();
|
|
354
|
+
}
|
|
288
355
|
rethrow;
|
|
289
356
|
}
|
|
290
357
|
}
|
|
@@ -323,6 +390,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
323
390
|
Future<Credentials> signinWithApple() async {
|
|
324
391
|
final appleProvider = AppleAuthProvider();
|
|
325
392
|
appleProvider.addScope('email');
|
|
393
|
+
if (kIsWeb && isMobileWebBrowser()) {
|
|
394
|
+
return _signInWithRedirectWeb(appleProvider);
|
|
395
|
+
}
|
|
326
396
|
try {
|
|
327
397
|
final value = kIsWeb
|
|
328
398
|
? await _popupOrCancel(() => _auth.signInWithPopup(appleProvider))
|
|
@@ -335,6 +405,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
335
405
|
if (e.code == 'canceled' || e.code == 'popup-closed-by-user' || e.code == 'cancelled-popup-request') {
|
|
336
406
|
throw const UserCancelledSignInException();
|
|
337
407
|
}
|
|
408
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
409
|
+
throw const EmailAlreadyRegisteredException();
|
|
410
|
+
}
|
|
338
411
|
rethrow;
|
|
339
412
|
}
|
|
340
413
|
}
|
|
@@ -345,6 +418,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
345
418
|
Future<Credentials> signupFromAnonymousWithApple() async {
|
|
346
419
|
final appleProvider = AppleAuthProvider();
|
|
347
420
|
appleProvider.addScope('email');
|
|
421
|
+
if (kIsWeb && isMobileWebBrowser()) {
|
|
422
|
+
return _signInWithRedirectWeb(appleProvider);
|
|
423
|
+
}
|
|
348
424
|
final currentUser = _auth.currentUser;
|
|
349
425
|
if (currentUser == null) {
|
|
350
426
|
// No anonymous session — fall back to regular sign-in.
|
|
@@ -357,6 +433,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
357
433
|
if (_isUserCancelledPopup(e.code)) {
|
|
358
434
|
throw const UserCancelledSignInException();
|
|
359
435
|
}
|
|
436
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
437
|
+
throw const EmailAlreadyRegisteredException();
|
|
438
|
+
}
|
|
360
439
|
rethrow;
|
|
361
440
|
}
|
|
362
441
|
}
|
|
@@ -397,6 +476,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
397
476
|
if (kIsWeb) {
|
|
398
477
|
final facebookProvider = FacebookAuthProvider();
|
|
399
478
|
facebookProvider.addScope('email');
|
|
479
|
+
if (isMobileWebBrowser()) {
|
|
480
|
+
return _signInWithRedirectWeb(facebookProvider);
|
|
481
|
+
}
|
|
400
482
|
final currentUser = _auth.currentUser;
|
|
401
483
|
if (currentUser == null) {
|
|
402
484
|
try {
|
|
@@ -447,6 +529,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
447
529
|
if (_isUserCancelledPopup(e.code)) {
|
|
448
530
|
throw const UserCancelledSignInException();
|
|
449
531
|
}
|
|
532
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
533
|
+
throw const EmailAlreadyRegisteredException();
|
|
534
|
+
}
|
|
450
535
|
rethrow;
|
|
451
536
|
}
|
|
452
537
|
}
|
|
@@ -475,6 +560,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
475
560
|
if (kIsWeb) {
|
|
476
561
|
final facebookProvider = FacebookAuthProvider();
|
|
477
562
|
facebookProvider.addScope('email');
|
|
563
|
+
if (isMobileWebBrowser()) {
|
|
564
|
+
return _signInWithRedirectWeb(facebookProvider);
|
|
565
|
+
}
|
|
478
566
|
try {
|
|
479
567
|
final value = await _popupOrCancel(
|
|
480
568
|
() => _auth.signInWithPopup(facebookProvider),
|
|
@@ -487,6 +575,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
487
575
|
if (_isUserCancelledPopup(e.code)) {
|
|
488
576
|
throw const UserCancelledSignInException();
|
|
489
577
|
}
|
|
578
|
+
if (e.code == 'account-exists-with-different-credential') {
|
|
579
|
+
throw const EmailAlreadyRegisteredException();
|
|
580
|
+
}
|
|
490
581
|
rethrow;
|
|
491
582
|
}
|
|
492
583
|
}
|
|
@@ -624,6 +715,120 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
624
715
|
Future<String?> getCurrentUserDisplayName() async =>
|
|
625
716
|
_auth.currentUser?.displayName;
|
|
626
717
|
|
|
718
|
+
@override
|
|
719
|
+
Future<String?> getCurrentUserPhotoUrl() async => _auth.currentUser?.photoURL;
|
|
720
|
+
|
|
721
|
+
@override
|
|
722
|
+
Future<List<String>> getLinkedProviders() async {
|
|
723
|
+
final providers = _auth.currentUser?.providerData;
|
|
724
|
+
if (providers == null) return const [];
|
|
725
|
+
return providers
|
|
726
|
+
.map((p) => switch (p.providerId) {
|
|
727
|
+
'google.com' => 'google',
|
|
728
|
+
'apple.com' => 'apple',
|
|
729
|
+
'facebook.com' => 'facebook',
|
|
730
|
+
'password' => 'email',
|
|
731
|
+
'phone' => 'phone',
|
|
732
|
+
_ => p.providerId,
|
|
733
|
+
})
|
|
734
|
+
.toList();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
@override
|
|
738
|
+
Future<void> setPassword(String password) async {
|
|
739
|
+
final user = _auth.currentUser;
|
|
740
|
+
if (user == null) throw Exception('No signed-in user');
|
|
741
|
+
final hasPassword =
|
|
742
|
+
user.providerData.any((p) => p.providerId == 'password');
|
|
743
|
+
if (hasPassword) {
|
|
744
|
+
// Already has an email/password credential: just change it.
|
|
745
|
+
await user.updatePassword(password);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// Social-only account: attach an email/password credential to the same UID.
|
|
749
|
+
final email = user.email;
|
|
750
|
+
if (email == null || email.isEmpty) {
|
|
751
|
+
throw Exception('Account has no email to attach a password to');
|
|
752
|
+
}
|
|
753
|
+
await user.linkWithCredential(
|
|
754
|
+
EmailAuthProvider.credential(email: email, password: password),
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
@override
|
|
759
|
+
Future<List<String>> linkableSocialProviders() async {
|
|
760
|
+
final user = _auth.currentUser;
|
|
761
|
+
if (user == null) return const [];
|
|
762
|
+
final linked = user.providerData
|
|
763
|
+
.map((p) => switch (p.providerId) {
|
|
764
|
+
'google.com' => 'google',
|
|
765
|
+
'apple.com' => 'apple',
|
|
766
|
+
'facebook.com' => 'facebook',
|
|
767
|
+
_ => p.providerId,
|
|
768
|
+
})
|
|
769
|
+
.toSet();
|
|
770
|
+
final result = <String>[];
|
|
771
|
+
if (!linked.contains('google')) result.add('google');
|
|
772
|
+
// Apple: native on iOS/macOS; on web only when the Apple web flow is set up.
|
|
773
|
+
final appleAvailable = kIsWeb
|
|
774
|
+
? withAppleWebSignin
|
|
775
|
+
: (defaultTargetPlatform == TargetPlatform.iOS ||
|
|
776
|
+
defaultTargetPlatform == TargetPlatform.macOS);
|
|
777
|
+
if (appleAvailable && !linked.contains('apple')) result.add('apple');
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
@override
|
|
782
|
+
Future<void> linkSocialProvider(String provider) async {
|
|
783
|
+
final user = _auth.currentUser;
|
|
784
|
+
if (user == null) throw Exception('No signed-in user');
|
|
785
|
+
switch (provider) {
|
|
786
|
+
case 'google':
|
|
787
|
+
if (kIsWeb) {
|
|
788
|
+
if (isMobileWebBrowser()) {
|
|
789
|
+
return _linkWithRedirectWeb(user, GoogleAuthProvider());
|
|
790
|
+
}
|
|
791
|
+
await _popupOrCancel(() => user.linkWithPopup(GoogleAuthProvider()));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
await GoogleSignIn.instance
|
|
796
|
+
.initialize(serverClientId: kGoogleWebClientId);
|
|
797
|
+
final googleUser = await GoogleSignIn.instance.authenticate();
|
|
798
|
+
final googleAuth = googleUser.authentication;
|
|
799
|
+
await user.linkWithCredential(
|
|
800
|
+
GoogleAuthProvider.credential(idToken: googleAuth.idToken),
|
|
801
|
+
);
|
|
802
|
+
} on GoogleSignInException catch (e) {
|
|
803
|
+
if (e.code == GoogleSignInExceptionCode.canceled) {
|
|
804
|
+
throw const UserCancelledSignInException();
|
|
805
|
+
}
|
|
806
|
+
rethrow;
|
|
807
|
+
}
|
|
808
|
+
case 'apple':
|
|
809
|
+
final appleProvider = AppleAuthProvider()..addScope('email');
|
|
810
|
+
if (kIsWeb && isMobileWebBrowser()) {
|
|
811
|
+
return _linkWithRedirectWeb(user, appleProvider);
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
if (kIsWeb) {
|
|
815
|
+
await _popupOrCancel(() => user.linkWithPopup(appleProvider));
|
|
816
|
+
} else {
|
|
817
|
+
await user.linkWithProvider(appleProvider);
|
|
818
|
+
}
|
|
819
|
+
} on FirebaseAuthException catch (e) {
|
|
820
|
+
if (e.code == 'canceled' ||
|
|
821
|
+
e.code == 'popup-closed-by-user' ||
|
|
822
|
+
e.code == 'cancelled-popup-request') {
|
|
823
|
+
throw const UserCancelledSignInException();
|
|
824
|
+
}
|
|
825
|
+
rethrow;
|
|
826
|
+
}
|
|
827
|
+
default:
|
|
828
|
+
throw Exception('Unsupported provider: $provider');
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
627
832
|
String _normalizePhoneNumber(String phoneNumber) {
|
|
628
833
|
String normalized = phoneNumber.replaceAll(RegExp(r'\D'), '');
|
|
629
834
|
if (!normalized.startsWith('+')) {
|
package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart
CHANGED
|
@@ -77,6 +77,28 @@ abstract class AuthenticationApi implements OnStartService {
|
|
|
77
77
|
|
|
78
78
|
/// Returns the display name of the currently authenticated Firebase Auth user (if any).
|
|
79
79
|
Future<String?> getCurrentUserDisplayName();
|
|
80
|
+
|
|
81
|
+
/// Returns the photo URL of the current user (e.g. Google profile picture), or null.
|
|
82
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
83
|
+
|
|
84
|
+
/// Returns ALL sign-in providers linked to the current account, each
|
|
85
|
+
/// normalised to 'google' | 'apple' | 'facebook' | 'email' | 'phone'.
|
|
86
|
+
/// Empty when there is no user or none can be determined.
|
|
87
|
+
Future<List<String>> getLinkedProviders();
|
|
88
|
+
|
|
89
|
+
/// Sets or updates an email/password credential for the current user, so a
|
|
90
|
+
/// social-only account can also sign in with email + password (same account,
|
|
91
|
+
/// no duplicate). Throws if there is no signed-in user.
|
|
92
|
+
Future<void> setPassword(String password);
|
|
93
|
+
|
|
94
|
+
/// Returns the social providers ('google'|'apple') the current user can still
|
|
95
|
+
/// link to their account (already-linked ones excluded). Empty on backends
|
|
96
|
+
/// that link identities automatically (e.g. Supabase).
|
|
97
|
+
Future<List<String>> linkableSocialProviders();
|
|
98
|
+
|
|
99
|
+
/// Links the given social provider ('google'|'apple') to the current account,
|
|
100
|
+
/// so it can also be used to sign in. Same account, no duplicate.
|
|
101
|
+
Future<void> linkSocialProvider(String provider);
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
class PhoneAlreadyLinkedException implements Exception {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import 'package:flutter/widgets.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/core/bottom_menu/active_tab_notifier.dart';
|
|
4
|
+
import 'package:kasy_kit/core/bottom_menu/web_url.dart';
|
|
5
|
+
import 'package:kasy_kit/router.dart';
|
|
6
|
+
|
|
7
|
+
/// Sends a freshly-authenticated user to Home — the single source of truth for
|
|
8
|
+
/// post-login navigation.
|
|
9
|
+
///
|
|
10
|
+
/// EVERY sign-in flow (email/password, Google, Google Play Games, Apple,
|
|
11
|
+
/// Facebook, phone and sign-up) calls this instead of navigating on its own, so
|
|
12
|
+
/// they all behave identically:
|
|
13
|
+
/// 1. force the next bottom-bar mount to Home — a fresh login must land on
|
|
14
|
+
/// Home, never on whatever tab (or stale web URL) a previous session left
|
|
15
|
+
/// behind. This is a one-shot flag, so it wins the race against the auth
|
|
16
|
+
/// page's own `redirectIfAuthenticated` (both navigate to `/`);
|
|
17
|
+
/// 2. navigate Home;
|
|
18
|
+
/// 3. on web, rewrite the address bar to `/` — the bottom bar (Bart) writes
|
|
19
|
+
/// tab URLs directly and never writes Home's, so without this the URL stays
|
|
20
|
+
/// frozen on the previous tab (e.g. `/settings`) while the screen is Home.
|
|
21
|
+
///
|
|
22
|
+
/// Adding a new sign-in method later? Call this one function and the tab reset
|
|
23
|
+
/// comes for free — there is nothing to remember and nothing to duplicate, which
|
|
24
|
+
/// is exactly what kept the old per-flow `go('/')` calls drifting out of sync.
|
|
25
|
+
void goHomeAfterLogin(Ref ref) {
|
|
26
|
+
requestHomeOnNextMount();
|
|
27
|
+
ref.read(goRouterProvider).go('/');
|
|
28
|
+
// After the frame that mounts the Home shell, correct the address bar. Home
|
|
29
|
+
// never re-pushes the URL, so this `replaceState('/')` sticks.
|
|
30
|
+
WidgetsBinding.instance
|
|
31
|
+
.addPostFrameCallback((_) => syncBrowserUrl('/'));
|
|
32
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import 'package:flutter/material.dart';
|
|
2
2
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
3
|
+
import 'package:kasy_kit/core/states/translations.dart';
|
|
3
4
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
4
5
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
5
6
|
import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
|
|
7
|
+
import 'package:kasy_kit/features/authentication/navigation/post_login_navigation.dart';
|
|
6
8
|
import 'package:kasy_kit/features/authentication/providers/models/phone_signin_state.dart';
|
|
7
9
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
8
10
|
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
9
|
-
import 'package:kasy_kit/router.dart';
|
|
10
11
|
import 'package:logger/logger.dart';
|
|
11
12
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
12
13
|
|
|
@@ -97,14 +98,13 @@ class PhoneAuthNotifier extends _$PhoneAuthNotifier {
|
|
|
97
98
|
false => await _authRepository.verifyPhoneAuth(verificationId, otp),
|
|
98
99
|
};
|
|
99
100
|
|
|
100
|
-
ref
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
text: "You are now signed in with your phone number",
|
|
101
|
+
final tr = ref.read(translationsProvider).phone_auth;
|
|
102
|
+
ref.read(toastProvider).success(
|
|
103
|
+
title: tr.signin_success_title,
|
|
104
|
+
text: tr.signin_success_text,
|
|
105
105
|
);
|
|
106
106
|
await Future.delayed(const Duration(seconds: 2));
|
|
107
|
-
ref
|
|
107
|
+
goHomeAfterLogin(ref);
|
|
108
108
|
} on PhoneAuthException catch (e) {
|
|
109
109
|
state = state.copyWith(isLoading: false, error: e.message);
|
|
110
110
|
} on PhoneAlreadyLinkedException {
|
|
@@ -3,12 +3,12 @@ import 'package:kasy_kit/core/data/models/user.dart';
|
|
|
3
3
|
import 'package:kasy_kit/core/states/translations.dart';
|
|
4
4
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
5
5
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
6
|
+
import 'package:kasy_kit/features/authentication/navigation/post_login_navigation.dart';
|
|
6
7
|
import 'package:kasy_kit/features/authentication/providers/models/email.dart';
|
|
7
8
|
import 'package:kasy_kit/features/authentication/providers/models/password.dart';
|
|
8
9
|
import 'package:kasy_kit/features/authentication/providers/models/signin_state.dart';
|
|
9
10
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
10
11
|
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
11
|
-
import 'package:kasy_kit/router.dart';
|
|
12
12
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
13
13
|
|
|
14
14
|
part 'signin_state_provider.g.dart';
|
|
@@ -54,7 +54,7 @@ class SigninStateNotifier extends _$SigninStateNotifier {
|
|
|
54
54
|
if (!ref.mounted) return;
|
|
55
55
|
await _userStateNotifier.onSignin();
|
|
56
56
|
if (!ref.mounted) return;
|
|
57
|
-
ref
|
|
57
|
+
goHomeAfterLogin(ref);
|
|
58
58
|
} catch (e, trace) {
|
|
59
59
|
debugPrint("Error while signing up: $e, $trace");
|
|
60
60
|
if (!ref.mounted) return;
|
|
@@ -84,13 +84,19 @@ class SigninStateNotifier extends _$SigninStateNotifier {
|
|
|
84
84
|
if (!ref.mounted) return;
|
|
85
85
|
await _userStateNotifier.onSignin();
|
|
86
86
|
if (!ref.mounted) return;
|
|
87
|
-
ref
|
|
87
|
+
goHomeAfterLogin(ref);
|
|
88
88
|
} catch (e, trace) {
|
|
89
89
|
if (!ref.mounted) return;
|
|
90
90
|
state = SigninState(email: state.email, password: state.password);
|
|
91
91
|
if (e is UserCancelledSignInException) return;
|
|
92
92
|
debugPrint("Error while signing up: $e, $trace");
|
|
93
|
-
ref.read(
|
|
93
|
+
final tr = ref.read(translationsProvider).auth.signin;
|
|
94
|
+
ref.read(toastProvider).error(
|
|
95
|
+
title: tr.error_title,
|
|
96
|
+
text: e is EmailAlreadyRegisteredException
|
|
97
|
+
? tr.email_already_registered
|
|
98
|
+
: tr.social_error(provider: 'Google'),
|
|
99
|
+
);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -104,13 +110,19 @@ class SigninStateNotifier extends _$SigninStateNotifier {
|
|
|
104
110
|
if (!ref.mounted) return;
|
|
105
111
|
await _userStateNotifier.onSignin();
|
|
106
112
|
if (!ref.mounted) return;
|
|
107
|
-
ref
|
|
113
|
+
goHomeAfterLogin(ref);
|
|
108
114
|
} catch (e, trace) {
|
|
109
115
|
if (!ref.mounted) return;
|
|
110
116
|
state = SigninState(email: state.email, password: state.password);
|
|
111
117
|
if (e is UserCancelledSignInException) return;
|
|
112
118
|
debugPrint("Error while signing up: $e, $trace");
|
|
113
|
-
ref.read(
|
|
119
|
+
final tr = ref.read(translationsProvider).auth.signin;
|
|
120
|
+
ref.read(toastProvider).error(
|
|
121
|
+
title: tr.error_title,
|
|
122
|
+
text: e is EmailAlreadyRegisteredException
|
|
123
|
+
? tr.email_already_registered
|
|
124
|
+
: tr.social_error(provider: 'Google Play Games'),
|
|
125
|
+
);
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
|
|
@@ -129,13 +141,19 @@ class SigninStateNotifier extends _$SigninStateNotifier {
|
|
|
129
141
|
if (!ref.mounted) return;
|
|
130
142
|
await _userStateNotifier.onSignin();
|
|
131
143
|
if (!ref.mounted) return;
|
|
132
|
-
ref
|
|
144
|
+
goHomeAfterLogin(ref);
|
|
133
145
|
} catch (e, trace) {
|
|
134
146
|
if (!ref.mounted) return;
|
|
135
147
|
state = SigninState(email: state.email, password: state.password);
|
|
136
148
|
if (e is UserCancelledSignInException) return;
|
|
137
149
|
debugPrint("Error while signing up: $e, $trace");
|
|
138
|
-
ref.read(
|
|
150
|
+
final tr = ref.read(translationsProvider).auth.signin;
|
|
151
|
+
ref.read(toastProvider).error(
|
|
152
|
+
title: tr.error_title,
|
|
153
|
+
text: e is EmailAlreadyRegisteredException
|
|
154
|
+
? tr.email_already_registered
|
|
155
|
+
: tr.social_error(provider: 'Apple'),
|
|
156
|
+
);
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
|
|
@@ -154,13 +172,19 @@ class SigninStateNotifier extends _$SigninStateNotifier {
|
|
|
154
172
|
if (!ref.mounted) return;
|
|
155
173
|
await _userStateNotifier.onSignin();
|
|
156
174
|
if (!ref.mounted) return;
|
|
157
|
-
ref
|
|
175
|
+
goHomeAfterLogin(ref);
|
|
158
176
|
} catch (e, trace) {
|
|
159
177
|
if (!ref.mounted) return;
|
|
160
178
|
state = SigninState(email: state.email, password: state.password);
|
|
161
179
|
if (e is UserCancelledSignInException) return;
|
|
162
180
|
debugPrint("Error while signing up: $e, $trace");
|
|
163
|
-
ref.read(
|
|
181
|
+
final tr = ref.read(translationsProvider).auth.signin;
|
|
182
|
+
ref.read(toastProvider).error(
|
|
183
|
+
title: tr.error_title,
|
|
184
|
+
text: e is EmailAlreadyRegisteredException
|
|
185
|
+
? tr.email_already_registered
|
|
186
|
+
: tr.social_error(provider: 'Facebook'),
|
|
187
|
+
);
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
}
|
|
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|
|
2
2
|
import 'package:kasy_kit/core/states/translations.dart';
|
|
3
3
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
4
4
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
5
|
+
import 'package:kasy_kit/features/authentication/navigation/post_login_navigation.dart';
|
|
5
6
|
import 'package:kasy_kit/features/authentication/providers/models/email.dart';
|
|
6
7
|
import 'package:kasy_kit/features/authentication/providers/models/password.dart';
|
|
7
8
|
import 'package:kasy_kit/features/authentication/providers/models/signup_state.dart';
|
|
8
9
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
9
|
-
import 'package:kasy_kit/router.dart';
|
|
10
10
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
11
11
|
|
|
12
12
|
part 'signup_state_provider.g.dart';
|
|
@@ -53,7 +53,7 @@ class SignupStateNotifier extends _$SignupStateNotifier {
|
|
|
53
53
|
await Future.delayed(const Duration(milliseconds: 1500));
|
|
54
54
|
await _userStateNotifier.onSignin();
|
|
55
55
|
if (!ref.mounted) return;
|
|
56
|
-
ref
|
|
56
|
+
goHomeAfterLogin(ref);
|
|
57
57
|
} catch (e, trace) {
|
|
58
58
|
debugPrint("Error while signing up: $e, $trace");
|
|
59
59
|
state = SignupState(email: state.email, password: state.password);
|
package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart
CHANGED
|
@@ -69,6 +69,23 @@ abstract class AuthenticationRepository {
|
|
|
69
69
|
/// Returns the display name of the authenticated Firebase Auth user.
|
|
70
70
|
Future<String?> getCurrentUserDisplayName();
|
|
71
71
|
|
|
72
|
+
/// Returns the photo URL of the current user (e.g. Google picture), or null.
|
|
73
|
+
Future<String?> getCurrentUserPhotoUrl();
|
|
74
|
+
|
|
75
|
+
/// Returns all sign-in providers linked to the current account
|
|
76
|
+
/// ('google'|'apple'|'facebook'|'email'|'phone'). Empty if none.
|
|
77
|
+
Future<List<String>> getLinkedProviders();
|
|
78
|
+
|
|
79
|
+
/// Sets or updates an email/password credential for the current user, so a
|
|
80
|
+
/// social-only account can also sign in with email + password.
|
|
81
|
+
Future<void> setPassword(String password);
|
|
82
|
+
|
|
83
|
+
/// Social providers the current user can still link to their account.
|
|
84
|
+
Future<List<String>> linkableSocialProviders();
|
|
85
|
+
|
|
86
|
+
/// Links a social provider ('google'|'apple') to the current account.
|
|
87
|
+
Future<void> linkSocialProvider(String provider);
|
|
88
|
+
|
|
72
89
|
/// Signin with Google Play Games account on Android
|
|
73
90
|
Future<void> signinWithGooglePlayGames();
|
|
74
91
|
|
|
@@ -263,6 +280,26 @@ class HttpAuthenticationRepository implements AuthenticationRepository {
|
|
|
263
280
|
Future<String?> getCurrentUserDisplayName() =>
|
|
264
281
|
_authenticationApi.getCurrentUserDisplayName();
|
|
265
282
|
|
|
283
|
+
@override
|
|
284
|
+
Future<String?> getCurrentUserPhotoUrl() =>
|
|
285
|
+
_authenticationApi.getCurrentUserPhotoUrl();
|
|
286
|
+
|
|
287
|
+
@override
|
|
288
|
+
Future<List<String>> getLinkedProviders() =>
|
|
289
|
+
_authenticationApi.getLinkedProviders();
|
|
290
|
+
|
|
291
|
+
@override
|
|
292
|
+
Future<void> setPassword(String password) =>
|
|
293
|
+
_authenticationApi.setPassword(password);
|
|
294
|
+
|
|
295
|
+
@override
|
|
296
|
+
Future<List<String>> linkableSocialProviders() =>
|
|
297
|
+
_authenticationApi.linkableSocialProviders();
|
|
298
|
+
|
|
299
|
+
@override
|
|
300
|
+
Future<void> linkSocialProvider(String provider) =>
|
|
301
|
+
_authenticationApi.linkSocialProvider(provider);
|
|
302
|
+
|
|
266
303
|
@override
|
|
267
304
|
Future<void> signinWithGooglePlayGames() async {
|
|
268
305
|
try {
|
|
@@ -70,6 +70,14 @@ class UserCancelledSignInException implements Exception {
|
|
|
70
70
|
const UserCancelledSignInException();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/// Thrown when a social sign-in is attempted with an email that already has an
|
|
74
|
+
/// account created with a different method (e.g. email/password or another
|
|
75
|
+
/// social provider). Lets the UI show a clear "email already in use" message
|
|
76
|
+
/// instead of a generic error, and avoids creating a duplicate account.
|
|
77
|
+
class EmailAlreadyRegisteredException implements Exception {
|
|
78
|
+
const EmailAlreadyRegisteredException();
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
class PhoneAuthException extends ApiError {
|
|
74
82
|
PhoneAuthException({
|
|
75
83
|
required super.code,
|
|
@@ -36,8 +36,8 @@ class _OtpVerificationComponentState
|
|
|
36
36
|
const SizedBox(height: KasySpacing.lg),
|
|
37
37
|
Text(
|
|
38
38
|
t.phone_auth.verification_code,
|
|
39
|
-
style: context.
|
|
40
|
-
|
|
39
|
+
style: context.kasyTextTheme.pageTitle.copyWith(
|
|
40
|
+
color: context.colors.onSurface,
|
|
41
41
|
),
|
|
42
42
|
textAlign: TextAlign.center,
|
|
43
43
|
),
|
|
@@ -38,8 +38,8 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
|
|
|
38
38
|
const SizedBox(height: KasySpacing.lg),
|
|
39
39
|
Text(
|
|
40
40
|
t.phone_auth.subtitle_input,
|
|
41
|
-
style: context.
|
|
42
|
-
|
|
41
|
+
style: context.kasyTextTheme.pageTitle.copyWith(
|
|
42
|
+
color: context.colors.onSurface,
|
|
43
43
|
),
|
|
44
44
|
textAlign: TextAlign.center,
|
|
45
45
|
),
|