207 lines
6.4 KiB
Dart
207 lines
6.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../config/api_config.dart';
|
|
import '../models/guess_score_summary.dart';
|
|
import '../models/incoming_question.dart';
|
|
import '../models/question_submit_result.dart';
|
|
import 'auth_service.dart';
|
|
|
|
/// HTTP client for question queue, answers, and deferrals.
|
|
class QuestionsApiService {
|
|
QuestionsApiService({http.Client? client}) : _client = client ?? http.Client();
|
|
|
|
final http.Client _client;
|
|
|
|
/// Ensures login question state is initialized for the signed-in user.
|
|
///
|
|
/// Returns the first unanswered question when present and always attempts to
|
|
/// return persisted [GuessScoreSummary] for this Firebase user when the API
|
|
/// includes it in the bootstrap payload.
|
|
Future<({IncomingQuestion? question, GuessScoreSummary? score})>
|
|
bootstrapOnLogin() async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return (question: null, score: null);
|
|
}
|
|
|
|
final http.Response response = await _client.post(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions/bootstrap'),
|
|
headers: _authHeaders(token),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'bootstrapOnLogin failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return (question: null, score: null);
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
final Map<String, dynamic>? questionJson =
|
|
body['question'] as Map<String, dynamic>?;
|
|
final Map<String, dynamic>? scoreJson =
|
|
body['score'] as Map<String, dynamic>?;
|
|
return (
|
|
question: questionJson == null
|
|
? null
|
|
: IncomingQuestion.fromJson(questionJson),
|
|
score: scoreJson == null
|
|
? null
|
|
: GuessScoreSummary.fromJson(scoreJson),
|
|
);
|
|
}
|
|
|
|
Future<List<IncomingQuestion>> fetchUnanswered() async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return <IncomingQuestion>[];
|
|
}
|
|
|
|
final http.Response response = await _client.get(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions'),
|
|
headers: _authHeaders(token),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'fetchUnanswered failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return <IncomingQuestion>[];
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
final List<dynamic> raw = body['questions'] as List<dynamic>? ?? <dynamic>[];
|
|
return raw
|
|
.map(
|
|
(dynamic item) =>
|
|
IncomingQuestion.fromJson(item as Map<String, dynamic>),
|
|
)
|
|
.toList();
|
|
}
|
|
|
|
Future<QuestionSubmitResult?> submitAnswer({
|
|
required String questionId,
|
|
num answer = 0,
|
|
}) async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return null;
|
|
}
|
|
|
|
final http.Response response = await _client.post(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/answer'),
|
|
headers: _authHeaders(token),
|
|
body: jsonEncode(<String, num>{'answer': answer}),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'submitAnswer failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
final Map<String, dynamic>? scoreJson =
|
|
body['score'] as Map<String, dynamic>?;
|
|
final Map<String, dynamic>? nextQuestionJson =
|
|
body['nextQuestion'] as Map<String, dynamic>?;
|
|
return QuestionSubmitResult(
|
|
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
|
|
score: scoreJson == null
|
|
? GuessScoreSummary.empty
|
|
: GuessScoreSummary.fromJson(scoreJson),
|
|
nextQuestion: nextQuestionJson == null
|
|
? null
|
|
: IncomingQuestion.fromJson(nextQuestionJson),
|
|
);
|
|
}
|
|
|
|
Future<int?> deferQuestion({required String questionId}) async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return null;
|
|
}
|
|
|
|
final http.Response response = await _client.post(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/defer'),
|
|
headers: _authHeaders(token),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'deferQuestion failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
return (body['unansweredCount'] as num?)?.toInt();
|
|
}
|
|
|
|
Future<GuessScoreSummary?> fetchGuessScoreSummary() async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return null;
|
|
}
|
|
|
|
final http.Response response = await _client.get(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions/score'),
|
|
headers: _authHeaders(token),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'fetchGuessScoreSummary failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
final Map<String, dynamic>? scoreJson =
|
|
body['score'] as Map<String, dynamic>?;
|
|
if (scoreJson == null) {
|
|
return GuessScoreSummary.empty;
|
|
}
|
|
return GuessScoreSummary.fromJson(scoreJson);
|
|
}
|
|
|
|
Future<GuessScoreSummary?> resetGuessScoreSummary() async {
|
|
final String? token = await AuthService.instance.getIdToken();
|
|
if (token == null) {
|
|
return null;
|
|
}
|
|
|
|
final http.Response response = await _client.post(
|
|
Uri.parse('$apiBaseUrl/v1/me/questions/score/reset'),
|
|
headers: _authHeaders(token),
|
|
);
|
|
if (response.statusCode != 200) {
|
|
debugPrint(
|
|
'resetGuessScoreSummary failed: ${response.statusCode} ${response.body}',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
final Map<String, dynamic> body =
|
|
jsonDecode(response.body) as Map<String, dynamic>;
|
|
final Map<String, dynamic>? scoreJson =
|
|
body['score'] as Map<String, dynamic>?;
|
|
if (scoreJson == null) {
|
|
return GuessScoreSummary.empty;
|
|
}
|
|
return GuessScoreSummary.fromJson(scoreJson);
|
|
}
|
|
|
|
Map<String, String> _authHeaders(String token) {
|
|
return <String, String>{
|
|
'Authorization': 'Bearer $token',
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
};
|
|
}
|
|
}
|