enum LoginChallengeSource { widget, uriQuery, rawSearch, rawHref, missing } class LoginChallengeResolution { final String? value; final LoginChallengeSource source; final bool uriHasLoginChallenge; final bool rawSearchHasLoginChallenge; final bool rawHrefHasLoginChallenge; const LoginChallengeResolution({ required this.value, required this.source, required this.uriHasLoginChallenge, required this.rawSearchHasLoginChallenge, required this.rawHrefHasLoginChallenge, }); Map toDiagnostics() { return { 'resolved_value_len': value?.length ?? 0, 'resolved_source': source.name, 'uri_has_login_challenge': uriHasLoginChallenge, 'raw_search_has_login_challenge': rawSearchHasLoginChallenge, 'raw_href_has_login_challenge': rawHrefHasLoginChallenge, }; } } LoginChallengeResolution resolveLoginChallenge({ String? widgetLoginChallenge, required Uri uri, String? rawSearch, String? rawHref, }) { final widgetValue = _normalizeChallenge(widgetLoginChallenge); if (widgetValue != null) { return const LoginChallengeResolution( value: null, source: LoginChallengeSource.widget, uriHasLoginChallenge: false, rawSearchHasLoginChallenge: false, rawHrefHasLoginChallenge: false, ).copyWith(value: widgetValue); } final uriValue = _normalizeChallenge(uri.queryParameters['login_challenge']); if (uriValue != null) { return const LoginChallengeResolution( value: null, source: LoginChallengeSource.uriQuery, uriHasLoginChallenge: true, rawSearchHasLoginChallenge: false, rawHrefHasLoginChallenge: false, ).copyWith(value: uriValue); } final rawSearchValue = _normalizeChallenge( _extractQueryParamFromRawQuery(rawSearch, 'login_challenge'), ); if (rawSearchValue != null) { return const LoginChallengeResolution( value: null, source: LoginChallengeSource.rawSearch, uriHasLoginChallenge: false, rawSearchHasLoginChallenge: true, rawHrefHasLoginChallenge: false, ).copyWith(value: rawSearchValue); } final rawHrefValue = _normalizeChallenge( _extractQueryParamFromRawHref(rawHref, 'login_challenge'), ); if (rawHrefValue != null) { return const LoginChallengeResolution( value: null, source: LoginChallengeSource.rawHref, uriHasLoginChallenge: false, rawSearchHasLoginChallenge: false, rawHrefHasLoginChallenge: true, ).copyWith(value: rawHrefValue); } return const LoginChallengeResolution( value: null, source: LoginChallengeSource.missing, uriHasLoginChallenge: false, rawSearchHasLoginChallenge: false, rawHrefHasLoginChallenge: false, ); } String? _normalizeChallenge(String? value) { final trimmed = value?.trim(); if (trimmed == null || trimmed.isEmpty) { return null; } return trimmed; } String? _extractQueryParamFromRawHref(String? rawHref, String key) { final href = rawHref?.trim(); if (href == null || href.isEmpty) { return null; } final parsed = Uri.tryParse(href); final fromParsed = parsed?.queryParameters[key]; final normalizedParsed = _normalizeChallenge(fromParsed); if (normalizedParsed != null) { return normalizedParsed; } final question = href.indexOf('?'); if (question < 0) { return null; } final hash = href.indexOf('#', question + 1); final rawQuery = hash < 0 ? href.substring(question + 1) : href.substring(question + 1, hash); return _extractQueryParamFromRawQuery(rawQuery, key); } String? _extractQueryParamFromRawQuery(String? rawQuery, String key) { final query = rawQuery?.trim(); if (query == null || query.isEmpty) { return null; } final normalizedQuery = query.startsWith('?') ? query.substring(1) : query; if (normalizedQuery.isEmpty) { return null; } try { final parsed = Uri.splitQueryString(normalizedQuery); final value = _normalizeChallenge(parsed[key]); if (value != null) { return value; } } catch (_) { // URI 파싱이 실패하면 수동 파싱으로 보완합니다. } for (final pair in normalizedQuery.split('&')) { if (pair.isEmpty) { continue; } final equalIndex = pair.indexOf('='); final rawKey = equalIndex < 0 ? pair : pair.substring(0, equalIndex); final decodedKey = _decodeQueryComponentSafe(rawKey); if (decodedKey != key) { continue; } if (equalIndex < 0) { return null; } final rawValue = pair.substring(equalIndex + 1); final decodedValue = _normalizeChallenge( _decodeQueryComponentSafe(rawValue), ); if (decodedValue != null) { return decodedValue; } } return null; } String _decodeQueryComponentSafe(String value) { try { return Uri.decodeQueryComponent(value); } catch (_) { return value; } } extension on LoginChallengeResolution { LoginChallengeResolution copyWith({ String? value, LoginChallengeSource? source, bool? uriHasLoginChallenge, bool? rawSearchHasLoginChallenge, bool? rawHrefHasLoginChallenge, }) { return LoginChallengeResolution( value: value ?? this.value, source: source ?? this.source, uriHasLoginChallenge: uriHasLoginChallenge ?? this.uriHasLoginChallenge, rawSearchHasLoginChallenge: rawSearchHasLoginChallenge ?? this.rawSearchHasLoginChallenge, rawHrefHasLoginChallenge: rawHrefHasLoginChallenge ?? this.rawHrefHasLoginChallenge, ); } }