forked from baron/baron-sso
flutter 린트 적용
This commit is contained in:
@@ -88,3 +88,16 @@ String buildLocalizedPath(String localeCode, Uri uri) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String buildSigninRedirectPath(String localeCode, Uri uri) {
|
||||||
|
final newPath = '/$localeCode/signin';
|
||||||
|
final newUri = uri.replace(path: newPath);
|
||||||
|
String result = newUri.path;
|
||||||
|
if (newUri.hasQuery) {
|
||||||
|
result += '?${newUri.query}';
|
||||||
|
}
|
||||||
|
if (newUri.hasFragment) {
|
||||||
|
result += '#${newUri.fragment}';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class AuthTokenStore {
|
|||||||
_localStorage.setItem(_providerKey, provider);
|
_localStorage.setItem(_providerKey, provider);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[AuthTokenStore] CRITICAL - Failed to set token: $e");
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,17 @@ class WebWindow {
|
|||||||
|
|
||||||
void redirectTo(String url) {
|
void redirectTo(String url) {
|
||||||
final currentHref = web.window.location.href;
|
final currentHref = web.window.location.href;
|
||||||
Uri? targetUri;
|
|
||||||
try {
|
|
||||||
targetUri = Uri.parse(url);
|
|
||||||
} catch (_) {
|
|
||||||
debugPrint("[WebWindow] redirectTo parse failed: url=$url");
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"[WebWindow] redirectTo start: current=$currentHref, target=$url",
|
"[WebWindow] redirectTo start: current=$currentHref, target=$url",
|
||||||
);
|
);
|
||||||
|
|
||||||
print("[WebWindow] FINAL REDIRECT ATTEMPT. URL: $url");
|
|
||||||
|
|
||||||
// Most direct and safe way for WASM: location.href assignment via package:web
|
// Most direct and safe way for WASM: location.href assignment via package:web
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
try {
|
try {
|
||||||
web.window.location.href = url;
|
web.window.location.href = url;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[WebWindow] CRITICAL JS ERROR: $e");
|
debugPrint("[WebWindow] CRITICAL JS ERROR: $e");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,10 @@ class LanguageSelector extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
LocaleStorage.write(value);
|
LocaleStorage.write(value);
|
||||||
await context.setLocale(Locale(value));
|
await context.setLocale(Locale(value));
|
||||||
|
if (!context.mounted) return;
|
||||||
final uri = GoRouterState.of(context).uri;
|
final uri = GoRouterState.of(context).uri;
|
||||||
final target = buildLocalizedPath(value, uri);
|
final target = buildLocalizedPath(value, uri);
|
||||||
if (context.mounted) {
|
context.go(target);
|
||||||
context.go(target);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import '../../../core/services/auth_token_store.dart';
|
|||||||
import '../../../core/services/oidc_redirect_guard.dart';
|
import '../../../core/services/oidc_redirect_guard.dart';
|
||||||
import '../../../core/notifiers/auth_notifier.dart';
|
import '../../../core/notifiers/auth_notifier.dart';
|
||||||
import '../domain/login_challenge_resolver.dart';
|
import '../domain/login_challenge_resolver.dart';
|
||||||
import '../domain/password_login_flow_policy.dart';
|
|
||||||
import '../../profile/domain/notifiers/profile_notifier.dart';
|
import '../../profile/domain/notifiers/profile_notifier.dart';
|
||||||
import '../../../core/services/web_window.dart';
|
import '../../../core/services/web_window.dart';
|
||||||
|
|
||||||
@@ -742,7 +741,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final localSessionMessage = tr(
|
final localSessionMessage = tr(
|
||||||
'msg.userfront.login.verification.approved_local',
|
'msg.userfront.login.verification.approved_local',
|
||||||
);
|
);
|
||||||
final linkLoginMessage = tr('msg.userfront.login.link.approved');
|
|
||||||
try {
|
try {
|
||||||
final res = await AuthProxyService.verifyLoginCode(
|
final res = await AuthProxyService.verifyLoginCode(
|
||||||
sanitizedLoginId,
|
sanitizedLoginId,
|
||||||
@@ -872,7 +870,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handlePasswordLogin() async {
|
Future<void> _handlePasswordLogin() async {
|
||||||
print("[Auth] _handlePasswordLogin START");
|
|
||||||
final input = _passwordLoginIdController.text.trim();
|
final input = _passwordLoginIdController.text.trim();
|
||||||
final password = _passwordController.text.trim();
|
final password = _passwordController.text.trim();
|
||||||
if (input.isEmpty || password.isEmpty) {
|
if (input.isEmpty || password.isEmpty) {
|
||||||
@@ -889,29 +886,23 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print("[Auth] Calling AuthProxyService.loginWithPassword...");
|
|
||||||
final res = await AuthProxyService.loginWithPassword(
|
final res = await AuthProxyService.loginWithPassword(
|
||||||
loginId,
|
loginId,
|
||||||
password,
|
password,
|
||||||
loginChallenge: _loginChallenge,
|
loginChallenge: _loginChallenge,
|
||||||
);
|
);
|
||||||
print("[Auth] loginWithPassword response: $res");
|
|
||||||
|
|
||||||
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
|
||||||
final provider = res['provider'] as String?;
|
final provider = res['provider'] as String?;
|
||||||
final redirectTo = res['redirectTo'] as String?;
|
final redirectTo = res['redirectTo'] as String?;
|
||||||
|
|
||||||
if (jwt != null) {
|
if (jwt != null) {
|
||||||
print("[Auth] JWT found, calling _onLoginSuccess. RedirectTo: $redirectTo");
|
|
||||||
_onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
|
_onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
|
||||||
} else if (redirectTo != null && redirectTo.isNotEmpty) {
|
} else if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||||
print("[Auth] Only redirectTo found. Redirecting...");
|
|
||||||
webWindow.redirectTo(redirectTo);
|
webWindow.redirectTo(redirectTo);
|
||||||
} else {
|
} else {
|
||||||
print("[Auth] No JWT and no redirectTo found.");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[Auth] _handlePasswordLogin Error: $e");
|
|
||||||
if (e.toString().contains("User not registered")) {
|
if (e.toString().contains("User not registered")) {
|
||||||
_showUnregisteredDialog();
|
_showUnregisteredDialog();
|
||||||
} else {
|
} else {
|
||||||
@@ -1134,56 +1125,44 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoginSuccess(String token, {String? provider, String? redirectTo}) async {
|
Future<void> _onLoginSuccess(String token, {String? provider, String? redirectTo}) async {
|
||||||
print("[Auth] _onLoginSuccess ENTRY. RedirectTo: $redirectTo, Token len: ${token.length}");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
print("[Auth] _onLoginSuccess: Not mounted, returning.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Priority 1] Immediate External Redirection
|
// [Priority 1] Immediate External Redirection
|
||||||
if (redirectTo != null && redirectTo.isNotEmpty) {
|
if (redirectTo != null && redirectTo.isNotEmpty) {
|
||||||
print("[Auth] _onLoginSuccess: Has redirectTo. Saving token and redirecting...");
|
|
||||||
try {
|
try {
|
||||||
final providerName = provider ?? AuthTokenStore.getProvider();
|
final providerName = provider ?? AuthTokenStore.getProvider();
|
||||||
print("[Auth] _onLoginSuccess: Provider resolved: $providerName");
|
|
||||||
AuthTokenStore.setToken(token, provider: providerName);
|
AuthTokenStore.setToken(token, provider: providerName);
|
||||||
print("[Auth] _onLoginSuccess: Token saved to store.");
|
|
||||||
} catch (stErr) {
|
} catch (stErr) {
|
||||||
print("[Auth] _onLoginSuccess: FAILED to save token: $stErr");
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
print("[Auth] Calling webWindow.redirectTo: $redirectTo");
|
|
||||||
webWindow.redirectTo(redirectTo); // Removed await as it's void
|
webWindow.redirectTo(redirectTo); // Removed await as it's void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Priority 2] OIDC Challenge Handling
|
// [Priority 2] OIDC Challenge Handling
|
||||||
if (_loginChallenge != null && _loginChallenge!.isNotEmpty) {
|
if (_loginChallenge != null && _loginChallenge!.isNotEmpty) {
|
||||||
print("[Auth] _onLoginSuccess: Has loginChallenge. Attempting auto-accept...");
|
|
||||||
try {
|
try {
|
||||||
// Save token first, it's needed for acceptance
|
// Save token first, it's needed for acceptance
|
||||||
final providerName = provider ?? AuthTokenStore.getProvider();
|
final providerName = provider ?? AuthTokenStore.getProvider();
|
||||||
AuthTokenStore.setToken(token, provider: providerName);
|
AuthTokenStore.setToken(token, provider: providerName);
|
||||||
print("[Auth] _onLoginSuccess: Token saved for auto-accept.");
|
|
||||||
|
|
||||||
final res = await AuthProxyService.acceptOidcLogin(
|
final res = await AuthProxyService.acceptOidcLogin(
|
||||||
_loginChallenge!,
|
_loginChallenge!,
|
||||||
token: token,
|
token: token,
|
||||||
);
|
);
|
||||||
final nextRedirectTo = res['redirectTo'] as String?;
|
final nextRedirectTo = res['redirectTo'] as String?;
|
||||||
print("[Auth] Auto-accept response: $res");
|
|
||||||
|
|
||||||
if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) {
|
if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) {
|
||||||
print("[Auth] OIDC login accepted. Redirecting to: $nextRedirectTo");
|
|
||||||
webWindow.redirectTo(nextRedirectTo); // Removed await
|
webWindow.redirectTo(nextRedirectTo); // Removed await
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
print("[Auth] Auto-accept successful but no redirectTo provided.");
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[Auth] Auto-accept failed: $e");
|
|
||||||
_showError(
|
_showError(
|
||||||
tr(
|
tr(
|
||||||
'msg.userfront.login.oidc_failed',
|
'msg.userfront.login.oidc_failed',
|
||||||
@@ -1193,7 +1172,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print("[Auth] _onLoginSuccess: Standard Login Flow");
|
|
||||||
_logTokenDetails(token);
|
_logTokenDetails(token);
|
||||||
|
|
||||||
final providerName = provider ?? AuthTokenStore.getProvider();
|
final providerName = provider ?? AuthTokenStore.getProvider();
|
||||||
@@ -1205,7 +1183,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
try {
|
try {
|
||||||
await ref.read(profileProvider.notifier).loadProfile();
|
await ref.read(profileProvider.notifier).loadProfile();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("[Auth] Failed to pre-fetch profile: $e");
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
final uri = Uri.base;
|
final uri = Uri.base;
|
||||||
@@ -1215,21 +1193,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
redirectParam != null && redirectParam.isNotEmpty;
|
redirectParam != null && redirectParam.isNotEmpty;
|
||||||
|
|
||||||
if (WebAuthIntegration.isPopup() || hasRedirectParam) {
|
if (WebAuthIntegration.isPopup() || hasRedirectParam) {
|
||||||
print(
|
|
||||||
"[Auth] External integration detected (popup or redirect). Notifying...",
|
|
||||||
);
|
|
||||||
WebAuthIntegration.sendLoginSuccess(token);
|
WebAuthIntegration.sendLoginSuccess(token);
|
||||||
AuthNotifier.instance.notify();
|
AuthNotifier.instance.notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
print("[Auth] Login success. Navigating to root.");
|
|
||||||
AuthNotifier.instance.notify();
|
AuthNotifier.instance.notify();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context.go('/');
|
context.go('/');
|
||||||
}
|
}
|
||||||
} catch (globalErr) {
|
} catch (globalErr) {
|
||||||
print("[Auth] CRITICAL ERROR in _onLoginSuccess: $globalErr");
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1286,10 +1286,12 @@ class _SignupScreenState extends State<SignupScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool canGoNext = false;
|
bool canGoNext = false;
|
||||||
if (_currentStep == 1 && _termsAccepted && _privacyAccepted)
|
if (_currentStep == 1 && _termsAccepted && _privacyAccepted) {
|
||||||
canGoNext = true;
|
canGoNext = true;
|
||||||
if (_currentStep == 2 && _isEmailVerified && _isPhoneVerified)
|
}
|
||||||
|
if (_currentStep == 2 && _isEmailVerified && _isPhoneVerified) {
|
||||||
canGoNext = true;
|
canGoNext = true;
|
||||||
|
}
|
||||||
if (_currentStep == 3) {
|
if (_currentStep == 3) {
|
||||||
final nameOk = _nameController.text.trim().isNotEmpty;
|
final nameOk = _nameController.text.trim().isNotEmpty;
|
||||||
if (_affiliationType == 'GENERAL') {
|
if (_affiliationType == 'GENERAL') {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -120,7 +121,6 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '', // Matches /:locale
|
path: '', // Matches /:locale
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
print("[Router] Building Dashboard (Root)");
|
|
||||||
return const DashboardScreen();
|
return const DashboardScreen();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -134,7 +134,6 @@ final _router = GoRouter(
|
|||||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
||||||
state.uri.queryParameters['redirect_url'];
|
state.uri.queryParameters['redirect_url'];
|
||||||
print("[Router] Building /signin. Challenge: $loginChallenge");
|
|
||||||
return LoginScreen(
|
return LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
@@ -149,7 +148,6 @@ final _router = GoRouter(
|
|||||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||||
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
final redirectUrl = state.uri.queryParameters['redirect_uri'] ??
|
||||||
state.uri.queryParameters['redirect_url'];
|
state.uri.queryParameters['redirect_url'];
|
||||||
print("[Router] Building /login (as signin). Challenge: $loginChallenge");
|
|
||||||
return LoginScreen(
|
return LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
@@ -162,12 +160,10 @@ final _router = GoRouter(
|
|||||||
builder: (BuildContext context, GoRouterState state) {
|
builder: (BuildContext context, GoRouterState state) {
|
||||||
final consentChallenge = state.uri.queryParameters['consent_challenge'];
|
final consentChallenge = state.uri.queryParameters['consent_challenge'];
|
||||||
if (consentChallenge == null) {
|
if (consentChallenge == null) {
|
||||||
print("[Router] WARNING: Consent screen without challenge.");
|
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(child: Text('Error: Consent challenge is missing.')),
|
body: Center(child: Text('Error: Consent challenge is missing.')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
print("[Router] Building /consent. Challenge: $consentChallenge");
|
|
||||||
return ConsentScreen(consentChallenge: consentChallenge);
|
return ConsentScreen(consentChallenge: consentChallenge);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -200,7 +196,6 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'l/:shortCode',
|
path: 'l/:shortCode',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final shortCode = state.pathParameters['shortCode'];
|
|
||||||
return LoginScreen(key: state.pageKey);
|
return LoginScreen(key: state.pageKey);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -264,11 +259,8 @@ final _router = GoRouter(
|
|||||||
final requestedLocale = extractLocaleFromPath(uri);
|
final requestedLocale = extractLocaleFromPath(uri);
|
||||||
final preferredLocale = resolvePreferredLocaleCode();
|
final preferredLocale = resolvePreferredLocaleCode();
|
||||||
|
|
||||||
print("[Router] Redirect check for: $uri");
|
|
||||||
|
|
||||||
if (requestedLocale == null) {
|
if (requestedLocale == null) {
|
||||||
final localizedPath = buildLocalizedPath(preferredLocale, uri);
|
final localizedPath = buildLocalizedPath(preferredLocale, uri);
|
||||||
print("[Router] Locale missing. Redirecting to: $localizedPath");
|
|
||||||
return localizedPath;
|
return localizedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,22 +288,12 @@ final _router = GoRouter(
|
|||||||
path.startsWith('/consent/') ||
|
path.startsWith('/consent/') ||
|
||||||
uri.path.contains('/consent');
|
uri.path.contains('/consent');
|
||||||
|
|
||||||
print("[Router] Path: $path, IsLoggedIn: $isLoggedIn, IsPublic: $isPublicPath");
|
|
||||||
|
|
||||||
if (isPublicPath) {
|
if (isPublicPath) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
print("[Router] ACCESS DENIED. Redirecting to /signin");
|
return buildSigninRedirectPath(requestedLocale, uri);
|
||||||
final locale = requestedLocale;
|
|
||||||
final newPath = '/$locale/signin';
|
|
||||||
|
|
||||||
// Preserve ALL query parameters
|
|
||||||
final finalRedirect = uri.replace(path: newPath);
|
|
||||||
String result = finalRedirect.path;
|
|
||||||
if (finalRedirect.hasQuery) result += '?${finalRedirect.query}';
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
// ignore_for_file: avoid_web_libraries_in_flutter
|
// ignore_for_file: avoid_web_libraries_in_flutter
|
||||||
|
|
||||||
import 'dart:html' as html;
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
class WebStorage {
|
class WebStorage {
|
||||||
bool get isWeb => true;
|
bool get isWeb => true;
|
||||||
|
|
||||||
String? get(String key) => html.window.localStorage[key];
|
String? get(String key) => web.window.localStorage.getItem(key);
|
||||||
|
|
||||||
void set(String key, String value) {
|
void set(String key, String value) {
|
||||||
html.window.localStorage[key] = value;
|
web.window.localStorage.setItem(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getSession(String key) => html.window.sessionStorage[key];
|
String? getSession(String key) => web.window.sessionStorage.getItem(key);
|
||||||
|
|
||||||
void setSession(String key, String value) {
|
void setSession(String key, String value) {
|
||||||
html.window.sessionStorage[key] = value;
|
web.window.sessionStorage.setItem(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSession(String key) {
|
void removeSession(String key) {
|
||||||
html.window.sessionStorage.remove(key);
|
web.window.sessionStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSession() {
|
void clearSession() {
|
||||||
html.window.sessionStorage.clear();
|
web.window.sessionStorage.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(String key) {
|
void remove(String key) {
|
||||||
html.window.localStorage.remove(key);
|
web.window.localStorage.removeItem(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
html.window.localStorage.clear();
|
web.window.localStorage.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user