feat(sbp_payments): added banks deep link handling to chat screen

This commit is contained in:
misha
2026-07-14 13:09:18 +03:00
parent be924265a1
commit 0ca2d1a1cc
10 changed files with 234 additions and 29 deletions

View File

@@ -123,7 +123,22 @@ class AppRouter {
return '${InfoByOrderScreen.path}?planId=$consultationId';
}
static String? _resolveDeepLinkUri(Uri uri) {
/// Deep link после оплаты Alfa: `finance/alfa/return/{orderId}` → чат с query `orderId`.
static String? _resolveFinanceAlfaReturnDeepLink(List<String> effectiveSegments) {
if (effectiveSegments.length != 4 ||
effectiveSegments[1] != 'alfa' ||
effectiveSegments[2] != 'return') {
return null;
}
final orderId = effectiveSegments[3];
if (orderId.isEmpty) return null;
developer.log('Deep link: finance/alfa/return/$orderId', name: 'AppRouter');
print('Deep link: finance/alfa/return/$orderId');
return '${RouteConstants.chatScreen}?orderId=$orderId';
}
/// Разбирает deep link URI в внутренний route target (или null, если host не наш).
static String? resolveDeepLinkTarget(Uri uri) {
final host = uri.host.toLowerCase();
final segments = uri.pathSegments;
@@ -189,6 +204,10 @@ class AppRouter {
final target = _resolveConsultationsDeepLink(effectiveSegments);
if (target != null) return target;
break;
case 'finance':
final target = _resolveFinanceAlfaReturnDeepLink(effectiveSegments);
if (target != null) return target;
break;
}
developer.log('Unknown deep link format: ${effectiveSegments.length} effective segments, type=$type, redirecting to feed', name: 'AppRouter');
@@ -196,7 +215,7 @@ class AppRouter {
}
static String? _handleRedirect(BuildContext context, GoRouterState state) {
return _resolveDeepLinkUri(state.uri);
return resolveDeepLinkTarget(state.uri);
}
static void navigateByDeepLinkUrl(String url) {
@@ -230,7 +249,7 @@ class AppRouter {
);
}
final target = _resolveDeepLinkUri(uri);
final target = resolveDeepLinkTarget(uri);
FlutterNativeSplash.remove();
if (target != null) {
appRouter.go(target);

View File

@@ -211,13 +211,23 @@ class OtherRoutes {
name: 'chatScreen',
builder: (context, state) {
final uuidFromQuery = state.uri.queryParameters['uuid'];
if (uuidFromQuery != null) {
return ChatScreen(uuid: uuidFromQuery, sendInitialMessage: false);
final orderIdFromQuery = state.uri.queryParameters['orderId'];
if (uuidFromQuery != null || orderIdFromQuery != null) {
return ChatScreen(
uuid: uuidFromQuery,
orderId: orderIdFromQuery,
sendInitialMessage: false,
);
}
final data = state.extra as Map<String, dynamic>;
final uuid = data['uuid'] as String;
final uuid = data['uuid'] as String?;
final orderId = data['orderId'] as String?;
final sendInitialMessage = data['sendInitialMessage'] as bool?;
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
return ChatScreen(
uuid: uuid,
orderId: orderId,
sendInitialMessage: sendInitialMessage,
);
},
),
GoRoute(

View File

@@ -91,17 +91,38 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> initChat(String uuid, {bool sendInitialMessage = false}) async {
/// Инициализирует чат по [uuid] либо по [orderId] (resolve → conversation_uuid).
///
/// При наличии [orderId] сначала вызывается
/// `GET /api/consultation/resolve/order_id_to_conversation/{orderId}`.
///
/// Возвращает conversation uuid, которым инициализировали чат (для reload без повторного resolve).
Future<String?> initChat(String? uuid, {String? orderId, bool sendInitialMessage = false}) async {
try {
emit(const ChatRoomState.loading());
final chatRoom = await chatRepository.getChatRoom(uuid);
ChosenPlanModel? chosenPlan;
if (chatRoom.chosenPlanId != null) {
chosenPlan = await chatRepository.getChosenPlan(id: chatRoom.chosenPlanId!);
String? resolvedUuid = uuid;
int? resolvedChosenPlanId;
if (orderId != null && orderId.isNotEmpty) {
final resolved = await chatRepository.resolveOrderIdToConversation(orderId: orderId);
resolvedUuid = resolved.conversationUuid;
resolvedChosenPlanId = resolved.chosenPlanId;
}
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage);
if (resolvedUuid == null || resolvedUuid.isEmpty) {
throw ArgumentError('initChat requires uuid or orderId');
}
final chatRoom = await chatRepository.getChatRoom(resolvedUuid);
ChosenPlanModel? chosenPlan;
final planId = resolvedChosenPlanId ?? chatRoom.chosenPlanId;
if (planId != null) {
chosenPlan = await chatRepository.getChosenPlan(id: planId);
}
final chatMessages = await chatRepository.getChatMessages(resolvedUuid, currentPage);
lastPage = chatMessages.meta.lastPage;
await _subscribeToChatChannel(uuid: uuid, chatId: chatRoom.id);
await _subscribeToChatChannel(uuid: resolvedUuid, chatId: chatRoom.id);
emit(
ChatRoomState.loaded(
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
@@ -115,6 +136,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
await checkCards();
}
return resolvedUuid;
} catch (e, st) {
await reportFrom(e, st, this);
log(e.toString());
@@ -122,6 +144,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
messagesSubscription?.cancel();
wsManager.unsubscribeDirectChatChannel();
emit(const ChatRoomState.error(errorMessage: 'Ошибка получеия данных'));
return null;
}
}

View File

@@ -36,10 +36,11 @@ import 'package:intl/intl.dart';
import 'package:visibility_detector/visibility_detector.dart';
class ChatScreen extends StatefulWidget {
final String uuid;
final String? uuid;
final String? orderId;
final bool? sendInitialMessage;
const ChatScreen({required this.uuid, super.key, required this.sendInitialMessage});
const ChatScreen({this.uuid, this.orderId, super.key, required this.sendInitialMessage});
@override
State<ChatScreen> createState() => _ChatScreenState();
@@ -55,6 +56,11 @@ class _ChatScreenState extends State<ChatScreen> {
late AppLifecycleListener _listener;
List<File> pickedFiles = [];
/// Conversation uuid после успешного init (в т.ч. после resolve по orderId).
String? _effectiveUuid;
String? get _conversationUuid => _effectiveUuid ?? widget.uuid;
void _onHide() {
chatRoomCubit.cancelTimer();
}
@@ -64,26 +70,42 @@ class _ChatScreenState extends State<ChatScreen> {
chatRoomCubit.setTimer();
}
Future<void> _initChatRoom() async {
final uuid = await chatRoomCubit.initChat(
_conversationUuid,
orderId: _effectiveUuid == null ? widget.orderId : null,
sendInitialMessage: widget.sendInitialMessage ?? false,
);
print('uuid: $uuid');
if (uuid != null && mounted) {
setState(() => _effectiveUuid = uuid);
}
}
@override
void didUpdateWidget(ChatScreen oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.uuid != widget.uuid) {
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
if (oldWidget.uuid != widget.uuid || oldWidget.orderId != widget.orderId) {
_effectiveUuid = widget.uuid;
_initChatRoom();
}
}
@override
void initState() {
super.initState();
_effectiveUuid = widget.uuid;
scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.uuid, sendInitialMessage: widget.sendInitialMessage ?? false);
_initChatRoom();
chatRoomCubit.setTimer();
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
void _onScroll() {
final uuid = _conversationUuid;
if (uuid == null) return;
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
chatRoomCubit.loadNextPage(widget.uuid);
chatRoomCubit.loadNextPage(uuid);
}
}
@@ -117,7 +139,7 @@ class _ChatScreenState extends State<ChatScreen> {
},
),
),
body: DataErrorWidget(onReload: () => chatRoomCubit.initChat(widget.uuid)),
body: DataErrorWidget(onReload: _initChatRoom),
);
} else {
return Scaffold(
@@ -242,7 +264,7 @@ class _ChatScreenState extends State<ChatScreen> {
GoRouter.of(context).pushNamed(
VideoCallScreen.name,
extra: <String, dynamic>{
'conversationUuid': widget.uuid,
'conversationUuid': _conversationUuid,
'counterpartName': state.chatRoom.users.receiver.name,
'localParticipantName': state.chatRoom.users.sender.name,
},