didvan-app/lib/services/saha/saha_service.dart

131 lines
4.3 KiB
Dart

// lib/services/saha/saha_service.dart
//
// Small client for the panel's `/delphi/my-forms` endpoint. Used from the
// home explore state to decide whether to render a Saha section (and to seed
// it with one card per panel). The card tap opens the existing `/saha`
// WebView page on the panel origin.
import 'dart:async';
import 'dart:convert';
import 'dart:io' show SocketException;
import 'package:flutter/foundation.dart' show debugPrint;
// ignore: depend_on_referenced_packages
import 'package:http/http.dart' as http;
import 'package:didvan/config/auth_config.dart';
import 'package:didvan/services/auth/token_storage.dart';
class SahaPanel {
final String id;
final String title;
final String? description;
final String? type;
const SahaPanel({
required this.id,
required this.title,
this.description,
this.type,
});
factory SahaPanel.fromJson(Map<String, dynamic> json) => SahaPanel(
id: json['id'] as String,
title: (json['title'] ?? '') as String,
description: json['description'] as String?,
type: json['type'] as String?,
);
}
class SahaService {
SahaService._();
static final SahaService instance = SahaService._();
static const _timeout = Duration(seconds: 15);
String get _base => AuthConfig.apiBaseUrl;
Future<List<SahaPanel>> fetchMyPanels() async {
final token = TokenStorage.accessToken;
if (token == null || token.isEmpty) {
debugPrint('🗳️ SAHA: no access token in TokenStorage');
return [];
}
try {
final res = await http.get(
Uri.parse('$_base/delphi/my-forms'),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
).timeout(_timeout);
if (res.statusCode != 200) {
final body = res.body.length > 300 ? res.body.substring(0, 300) : res.body;
debugPrint('🗳️ SAHA: my-forms HTTP ${res.statusCode}: $body');
return [];
}
final decoded = json.decode(res.body);
if (decoded is List) {
return decoded
.whereType<Map>()
.map((m) => SahaPanel.fromJson(Map<String, dynamic>.from(m)))
.toList();
}
debugPrint('🗳️ SAHA: my-forms returned non-list payload: '
'${res.body.length > 300 ? res.body.substring(0, 300) : res.body}');
return [];
} on TimeoutException {
debugPrint('🗳️ SAHA: my-forms timed out');
return [];
} on SocketException catch (e) {
debugPrint('🗳️ SAHA: my-forms network error: $e');
return [];
} catch (e) {
debugPrint('🗳️ SAHA: fetchMyPanels error: $e');
return [];
}
}
/// Signs a magic-link invite token for the current Keycloak user on a
/// specific panel and returns the ready-to-open respondent URL, or null
/// on failure. The URL points at the panel origin so `Routes.web` can
/// open it in the cookie-injecting WebView.
Future<String?> buildMyPanelUrl(String panelId) async {
final token = TokenStorage.accessToken;
if (token == null || token.isEmpty) {
debugPrint('🗳️ SAHA: my-link no access token');
return null;
}
try {
final res = await http.post(
Uri.parse('$_base/delphi/forms/$panelId/my-link'),
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer $token',
},
).timeout(_timeout);
if (res.statusCode != 200 && res.statusCode != 201) {
final body = res.body.length > 300 ? res.body.substring(0, 300) : res.body;
debugPrint('🗳️ SAHA: my-link HTTP ${res.statusCode}: $body');
return null;
}
final decoded = json.decode(res.body);
if (decoded is! Map || decoded['token'] is! String) {
debugPrint('🗳️ SAHA: my-link bad payload: ${res.body}');
return null;
}
final t = Uri.encodeComponent(decoded['token'] as String);
return 'https://app.didvan.com/panel/delphi/forms/user/$panelId?token=$t';
} on TimeoutException {
debugPrint('🗳️ SAHA: my-link timed out');
return null;
} on SocketException catch (e) {
debugPrint('🗳️ SAHA: my-link network error: $e');
return null;
} catch (e) {
debugPrint('🗳️ SAHA: buildMyPanelUrl error: $e');
return null;
}
}
}