mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-19 00:02:15 +02:00
fix(extensions): preserve verification callback retries
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/services/app_navigation_service.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/extension_session_callback.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -359,6 +360,7 @@ Future<bool> showExtensionVerificationHelpDialog(
|
||||
await _completeSessionGrantFromClipboard(
|
||||
dialogContext,
|
||||
extensionId,
|
||||
authUri,
|
||||
);
|
||||
if (dialogContext.mounted) {
|
||||
setDialogState(() => clipboardGrantInFlight = false);
|
||||
@@ -389,15 +391,13 @@ Future<bool> showExtensionVerificationHelpDialog(
|
||||
Future<void> _completeSessionGrantFromClipboard(
|
||||
BuildContext context,
|
||||
String extensionId,
|
||||
Uri verificationUri,
|
||||
) async {
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
try {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = data?.text?.trim() ?? '';
|
||||
final parsed = _parseSessionGrantCallback(
|
||||
text,
|
||||
fallbackExtensionId: extensionId,
|
||||
);
|
||||
final parsed = parseExtensionSessionGrantCallback(text);
|
||||
if (parsed == null) {
|
||||
messenger?.showSnackBar(
|
||||
const SnackBar(content: Text('No verification callback found')),
|
||||
@@ -405,8 +405,20 @@ Future<void> _completeSessionGrantFromClipboard(
|
||||
return;
|
||||
}
|
||||
|
||||
final expectedState = extensionCallbackStateFromVerificationUri(
|
||||
verificationUri,
|
||||
);
|
||||
if (expectedState == null || parsed.state != expectedState) {
|
||||
messenger?.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('This callback does not match the active verification'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final success = await PlatformBridge.completeExtensionSessionGrant(
|
||||
parsed.extensionId,
|
||||
extensionId.trim(),
|
||||
parsed.grant,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
@@ -429,50 +441,6 @@ Future<void> _completeSessionGrantFromClipboard(
|
||||
}
|
||||
}
|
||||
|
||||
({String extensionId, String grant})? _parseSessionGrantCallback(
|
||||
String text, {
|
||||
required String fallbackExtensionId,
|
||||
}) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
String? grant;
|
||||
String? state;
|
||||
final uri = Uri.tryParse(trimmed);
|
||||
if (uri != null) {
|
||||
grant = uri.queryParameters['grant'] ?? uri.queryParameters['code'];
|
||||
state = uri.queryParameters['state'];
|
||||
|
||||
final nestedCallback = uri.queryParameters['cb'];
|
||||
if ((grant == null || grant.trim().isEmpty) &&
|
||||
nestedCallback != null &&
|
||||
nestedCallback.trim().isNotEmpty) {
|
||||
final nested = _parseSessionGrantCallback(
|
||||
nestedCallback,
|
||||
fallbackExtensionId: fallbackExtensionId,
|
||||
);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
|
||||
grant ??= _firstRegexGroup(trimmed, RegExp(r'(?:^|[?&#\s])grant=([^&#\s]+)'));
|
||||
grant ??= _firstRegexGroup(trimmed, RegExp(r'(?:^|[?&#\s])code=([^&#\s]+)'));
|
||||
state ??= _firstRegexGroup(trimmed, RegExp(r'(?:^|[?&#\s])state=([^&#\s]+)'));
|
||||
|
||||
grant = grant == null ? null : Uri.decodeComponent(grant.trim());
|
||||
state = state == null ? null : Uri.decodeComponent(state.trim());
|
||||
final extension = (state != null && state.isNotEmpty)
|
||||
? state
|
||||
: fallbackExtensionId.trim();
|
||||
if (extension.isEmpty || grant == null || grant.isEmpty) return null;
|
||||
return (extensionId: extension, grant: grant);
|
||||
}
|
||||
|
||||
String? _firstRegexGroup(String input, RegExp regex) {
|
||||
final match = regex.firstMatch(input);
|
||||
return match?.group(1);
|
||||
}
|
||||
|
||||
/// Opens an extension auth/verification page. On iOS this prefers an
|
||||
/// ASWebAuthenticationSession, which captures the spotiflac:// callback
|
||||
/// in-process — required for environments where the app's URL scheme is not
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
typedef ExtensionSessionGrantCallback = ({String grant, String state});
|
||||
|
||||
ExtensionSessionGrantCallback? parseExtensionSessionGrantCallback(String text) {
|
||||
final trimmed = text.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
String? grant;
|
||||
String? state;
|
||||
final uri = Uri.tryParse(trimmed);
|
||||
if (uri != null) {
|
||||
grant = _nonEmpty(
|
||||
uri.queryParameters['grant'] ?? uri.queryParameters['code'],
|
||||
);
|
||||
state = _nonEmpty(uri.queryParameters['state']);
|
||||
|
||||
final nestedCallback = _nonEmpty(uri.queryParameters['cb']);
|
||||
if (grant == null && nestedCallback != null) {
|
||||
final nested = parseExtensionSessionGrantCallback(nestedCallback);
|
||||
if (nested != null) return nested;
|
||||
}
|
||||
}
|
||||
|
||||
grant ??= _decodedRegexGroup(
|
||||
trimmed,
|
||||
RegExp(r'(?:^|[?&#\s])grant=([^&#\s]+)'),
|
||||
);
|
||||
grant ??= _decodedRegexGroup(
|
||||
trimmed,
|
||||
RegExp(r'(?:^|[?&#\s])code=([^&#\s]+)'),
|
||||
);
|
||||
state ??= _decodedRegexGroup(
|
||||
trimmed,
|
||||
RegExp(r'(?:^|[?&#\s])state=([^&#\s]+)'),
|
||||
);
|
||||
|
||||
grant = _nonEmpty(grant);
|
||||
state = _nonEmpty(state);
|
||||
if (grant == null || state == null) return null;
|
||||
return (grant: grant, state: state);
|
||||
}
|
||||
|
||||
String? extensionCallbackStateFromVerificationUri(Uri uri, [int depth = 0]) {
|
||||
if (depth > 3) return null;
|
||||
|
||||
final directState = _nonEmpty(uri.queryParameters['state']);
|
||||
if (directState != null) return directState;
|
||||
|
||||
for (final key in const ['cb', 'callback', 'callback_url', 'redirect_uri']) {
|
||||
final nestedText = _nonEmpty(uri.queryParameters[key]);
|
||||
if (nestedText == null) continue;
|
||||
final nestedUri = Uri.tryParse(nestedText);
|
||||
if (nestedUri == null) continue;
|
||||
final nestedState = extensionCallbackStateFromVerificationUri(
|
||||
nestedUri,
|
||||
depth + 1,
|
||||
);
|
||||
if (nestedState != null) return nestedState;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _decodedRegexGroup(String input, RegExp regex) {
|
||||
final value = _nonEmpty(regex.firstMatch(input)?.group(1));
|
||||
if (value == null) return null;
|
||||
try {
|
||||
return Uri.decodeComponent(value);
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _nonEmpty(String? value) {
|
||||
final trimmed = value?.trim() ?? '';
|
||||
return trimmed.isEmpty ? null : trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user