Initial web socket client support
This commit is contained in:
		
							parent
							
								
									e7641f6aec
								
							
						
					
					
						commit
						623474e0c6
					
				@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
import 'package:frontend/providers/auth.dart';
 | 
			
		||||
import 'package:frontend/providers/game_messages.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
 | 
			
		||||
@ -18,25 +19,19 @@ class GameRoomHome extends ConsumerStatefulWidget {
 | 
			
		||||
class _GameRoomHomeState extends ConsumerState<GameRoomHome> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final jwtAsync = ref.watch(jwtNotifierProvider);
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      body: jwtAsync.when(
 | 
			
		||||
        data: (jwt) {
 | 
			
		||||
          if (jwt == null || jwt.roomUuid != widget.roomUuid) {
 | 
			
		||||
            logger.fine('Tried to open room, but not authenticated / wrong room');
 | 
			
		||||
            // return home
 | 
			
		||||
            context.go('/');
 | 
			
		||||
          }
 | 
			
		||||
          return Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('Authenticated.'),
 | 
			
		||||
              Text('Welcome to room ${widget.roomUuid}'),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
        loading: () => CircularProgressIndicator(),
 | 
			
		||||
        error: (e, st) => Text('$e, $st'),
 | 
			
		||||
      ),
 | 
			
		||||
    final jwt = ref.watch(jwtBodyProvider);
 | 
			
		||||
    if (jwt == null || jwt.roomUuid != widget.roomUuid) {
 | 
			
		||||
      logger.fine('Tried to open room, but not authenticated / wrong room');
 | 
			
		||||
      // return home
 | 
			
		||||
      context.go('/');
 | 
			
		||||
    }
 | 
			
		||||
    // enstablish ws connection at /room/roomCode/ws and save to gameMessageProvider
 | 
			
		||||
    ref.read(gameMessageNotifierProvider.notifier).connect(jwt!.roomUuid);
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        Text('Authenticated.'),
 | 
			
		||||
        Text('Welcome to room ${widget.roomUuid}'),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
import 'package:jwt_decoder/jwt_decoder.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
@ -11,7 +12,7 @@ final logger = Logger('provider/auth');
 | 
			
		||||
@riverpod
 | 
			
		||||
class JwtNotifier extends _$JwtNotifier {
 | 
			
		||||
  @override
 | 
			
		||||
  Future<JWTBody?> build() async {
 | 
			
		||||
  Future<String?> build() async {
 | 
			
		||||
    if (!await SharedPreferencesAsync().containsKey('jwt')) {
 | 
			
		||||
      logger.fine('No JWT saved to client');
 | 
			
		||||
      return null;
 | 
			
		||||
@ -23,20 +24,7 @@ class JwtNotifier extends _$JwtNotifier {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final payload = JwtDecoder.tryDecode(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;
 | 
			
		||||
    }
 | 
			
		||||
    return jwtString;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> setJwt(String jwt) async {
 | 
			
		||||
@ -50,11 +38,30 @@ class JwtNotifier extends _$JwtNotifier {
 | 
			
		||||
    logger.fine('Saving jwt token to shared prefs');
 | 
			
		||||
    await SharedPreferencesAsync().setString('jwt', jwt);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final jwtBody = JWTBody.fromJson(payload);
 | 
			
		||||
      state = AsyncValue.data(jwtBody);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      state = AsyncError(e, StackTrace.current);
 | 
			
		||||
    }
 | 
			
		||||
    state = AsyncValue.data(jwt);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@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:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
import 'package:frontend/providers/auth.dart';
 | 
			
		||||
import 'package:logging/logging.dart';
 | 
			
		||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
			
		||||
 | 
			
		||||
@ -15,11 +16,30 @@ Dio dio(Ref ref) {
 | 
			
		||||
    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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
class CustomLogInterceptor extends Interceptor {
 | 
			
		||||
  @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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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