Initial web socket client support
This commit is contained in:
parent
e7641f6aec
commit
623474e0c6
frontend/lib
shared_models/lib
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:frontend/providers/auth.dart';
|
import 'package:frontend/providers/auth.dart';
|
||||||
|
import 'package:frontend/providers/game_messages.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@ -18,25 +19,19 @@ class GameRoomHome extends ConsumerStatefulWidget {
|
|||||||
class _GameRoomHomeState extends ConsumerState<GameRoomHome> {
|
class _GameRoomHomeState extends ConsumerState<GameRoomHome> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final jwtAsync = ref.watch(jwtNotifierProvider);
|
final jwt = ref.watch(jwtBodyProvider);
|
||||||
return Scaffold(
|
|
||||||
body: jwtAsync.when(
|
|
||||||
data: (jwt) {
|
|
||||||
if (jwt == null || jwt.roomUuid != widget.roomUuid) {
|
if (jwt == null || jwt.roomUuid != widget.roomUuid) {
|
||||||
logger.fine('Tried to open room, but not authenticated / wrong room');
|
logger.fine('Tried to open room, but not authenticated / wrong room');
|
||||||
// return home
|
// return home
|
||||||
context.go('/');
|
context.go('/');
|
||||||
}
|
}
|
||||||
|
// enstablish ws connection at /room/roomCode/ws and save to gameMessageProvider
|
||||||
|
ref.read(gameMessageNotifierProvider.notifier).connect(jwt!.roomUuid);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text('Authenticated.'),
|
Text('Authenticated.'),
|
||||||
Text('Welcome to room ${widget.roomUuid}'),
|
Text('Welcome to room ${widget.roomUuid}'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
loading: () => CircularProgressIndicator(),
|
|
||||||
error: (e, st) => Text('$e, $st'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@ -11,7 +12,7 @@ final logger = Logger('provider/auth');
|
|||||||
@riverpod
|
@riverpod
|
||||||
class JwtNotifier extends _$JwtNotifier {
|
class JwtNotifier extends _$JwtNotifier {
|
||||||
@override
|
@override
|
||||||
Future<JWTBody?> build() async {
|
Future<String?> build() async {
|
||||||
if (!await SharedPreferencesAsync().containsKey('jwt')) {
|
if (!await SharedPreferencesAsync().containsKey('jwt')) {
|
||||||
logger.fine('No JWT saved to client');
|
logger.fine('No JWT saved to client');
|
||||||
return null;
|
return null;
|
||||||
@ -23,20 +24,7 @@ class JwtNotifier extends _$JwtNotifier {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final payload = JwtDecoder.tryDecode(jwtString);
|
return jwtString;
|
||||||
if (payload == null) {
|
|
||||||
logger.fine('Failed to decode JWT, removing key.');
|
|
||||||
SharedPreferencesAsync().remove('jwt');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final body = JWTBody.fromJson(payload);
|
|
||||||
return body;
|
|
||||||
} catch (e) {
|
|
||||||
logger.shout('Failed to parse JWT payload to JWTBody, something is wrong:', e, StackTrace.current);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setJwt(String jwt) async {
|
Future<void> setJwt(String jwt) async {
|
||||||
@ -50,11 +38,30 @@ class JwtNotifier extends _$JwtNotifier {
|
|||||||
logger.fine('Saving jwt token to shared prefs');
|
logger.fine('Saving jwt token to shared prefs');
|
||||||
await SharedPreferencesAsync().setString('jwt', jwt);
|
await SharedPreferencesAsync().setString('jwt', jwt);
|
||||||
|
|
||||||
try {
|
state = AsyncValue.data(jwt);
|
||||||
final jwtBody = JWTBody.fromJson(payload);
|
}
|
||||||
state = AsyncValue.data(jwtBody);
|
}
|
||||||
} catch (e) {
|
|
||||||
state = AsyncError(e, StackTrace.current);
|
@riverpod
|
||||||
}
|
JWTBody? jwtBody(Ref ref) {
|
||||||
|
final jwtString = ref.watch(jwtNotifierProvider).valueOrNull;
|
||||||
|
if (jwtString == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final payload = JwtDecoder.tryDecode(jwtString);
|
||||||
|
if (payload == null) {
|
||||||
|
logger.fine('Failed to decode JWT, removing key.');
|
||||||
|
SharedPreferencesAsync().remove('jwt');
|
||||||
|
ref.invalidate(jwtNotifierProvider);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final body = JWTBody.fromJson(payload);
|
||||||
|
return body;
|
||||||
|
} catch (e) {
|
||||||
|
logger.shout(
|
||||||
|
'Failed to parse JWT payload to JWTBody, something is wrong.\nPayload: $payload', e, StackTrace.current);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frontend/providers/auth.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@ -15,11 +16,30 @@ Dio dio(Ref ref) {
|
|||||||
receiveTimeout: const Duration(seconds: 3),
|
receiveTimeout: const Duration(seconds: 3),
|
||||||
));
|
));
|
||||||
|
|
||||||
dio.interceptors.add(LogInterceptor(responseBody: true));
|
final jwt = ref.watch(jwtNotifierProvider).valueOrNull;
|
||||||
|
|
||||||
|
dio.interceptors.addAll([JwtInterceptor(jwt: jwt), CustomLogInterceptor()]);
|
||||||
|
|
||||||
|
logger.fine('Created new Dio object');
|
||||||
|
|
||||||
return dio;
|
return dio;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adds the jwt to
|
||||||
|
class JwtInterceptor extends Interceptor {
|
||||||
|
final String? jwt;
|
||||||
|
|
||||||
|
JwtInterceptor({required this.jwt});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||||
|
if (jwt != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $jwt';
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a custom LogInterceptor using the logger object
|
// Create a custom LogInterceptor using the logger object
|
||||||
class CustomLogInterceptor extends Interceptor {
|
class CustomLogInterceptor extends Interceptor {
|
||||||
@override
|
@override
|
||||||
|
43
frontend/lib/providers/game_messages.dart
Normal file
43
frontend/lib/providers/game_messages.dart
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:frontend/providers/dio.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shared_models/room.dart';
|
||||||
|
|
||||||
|
part 'game_messages.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class GameMessageNotifier extends _$GameMessageNotifier {
|
||||||
|
@override
|
||||||
|
Stream<GameRoomMessage> build() {
|
||||||
|
return Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> connect(String gameRoomUuid) async {
|
||||||
|
final dio = ref.read(dioProvider);
|
||||||
|
Uri.parse('ws://localhost:8080/room/$gameRoomUuid/ws');
|
||||||
|
|
||||||
|
// connect to websocket and then set stream of websocket to state
|
||||||
|
final wsUrl = Uri.parse('ws://localhost:8080/room/$gameRoomUuid/ws');
|
||||||
|
|
||||||
|
final connection = await WebSocket.connect(wsUrl.toString());
|
||||||
|
|
||||||
|
final Stream<GameRoomMessage> gameRoomStream = connection.map((message) {
|
||||||
|
try {
|
||||||
|
if (message is String) {
|
||||||
|
GameRoomMessage.fromJson(jsonDecode(message) as Map<String, dynamic>);
|
||||||
|
} else {
|
||||||
|
logger.info('Recieved non-string message in socket: $message');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error parsing message: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gameRoomStream.listen(
|
||||||
|
(event) => state = AsyncValue.data(event),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -23,3 +23,51 @@ class CreateRoomResponse {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$CreateRoomResponseToJson(this);
|
Map<String, dynamic> toJson() => _$CreateRoomResponseToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class GameRoomMessage {
|
||||||
|
const GameRoomMessage();
|
||||||
|
|
||||||
|
factory GameRoomMessage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return switch (json['type']) {
|
||||||
|
'ping' => PingMessage.fromJson(json),
|
||||||
|
'playerVote' => PlayerVoteMessage.fromJson(json),
|
||||||
|
_ => throw Exception('Unknown message type'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PingMessage extends GameRoomMessage {
|
||||||
|
final DateTime timestamp;
|
||||||
|
|
||||||
|
const PingMessage({required this.timestamp});
|
||||||
|
|
||||||
|
factory PingMessage.fromJson(Map<String, dynamic> json) =>
|
||||||
|
PingMessage(timestamp: DateTime.parse(json['timestamp'] as String));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'type': 'ping',
|
||||||
|
'timestamp': timestamp.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerVoteMessage extends GameRoomMessage {
|
||||||
|
final String playerUuid;
|
||||||
|
final int vote;
|
||||||
|
|
||||||
|
const PlayerVoteMessage({required this.playerUuid, required this.vote});
|
||||||
|
|
||||||
|
factory PlayerVoteMessage.fromJson(Map<String, dynamic> json) => PlayerVoteMessage(
|
||||||
|
playerUuid: json['playerUuid'] as String,
|
||||||
|
vote: json['vote'] as int,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'type': 'playerVote',
|
||||||
|
'playerUuid': playerUuid,
|
||||||
|
'vote': vote,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user