Billing upgrade/downgrade, inviti in app e diretta YouTube da mobile.

Upgrade Stripe con addebito immediato; downgrade programmato a fine periodo.
UX billing più sobria con box info; icone piani. API inviti e OAuth YouTube
per staff trasmissione; deep link join; scelta partita programmata o nuova.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 22:15:27 +02:00
parent 2cb267f991
commit 566104aff4
49 changed files with 1615 additions and 173 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/config.dart';
import '../providers/auth_provider.dart';
import 'models/invite_preview.dart';
import 'models/match.dart';
import 'models/recording_item.dart';
import 'models/stream_session.dart';
@@ -86,6 +87,24 @@ class ApiClient {
.toList();
}
Future<InvitePreview> fetchInvitePreview(String token) async {
final response = await _dio.get<Map<String, dynamic>>('/invitations/$token');
return InvitePreview.fromJson(response.data!);
}
Future<String> acceptInvitation(String token) async {
final response = await _dio.post<Map<String, dynamic>>('/invitations/$token/accept');
return response.data!['message'] as String? ?? 'Invito accettato';
}
Future<String> youtubeAuthorizeUrl(String teamId, {bool returnApp = true}) async {
final response = await _dio.get<Map<String, dynamic>>(
'/teams/$teamId/youtube/authorize',
queryParameters: returnApp ? {'return_app': '1'} : null,
);
return response.data!['authorization_url'] as String;
}
Future<List<RecordingItem>> fetchRecordings(String teamId) async {
final response = await _dio.get<List<dynamic>>('/teams/$teamId/recordings');
return response.data!

View File

@@ -0,0 +1,28 @@
class InvitePreview {
const InvitePreview({
required this.valid,
required this.email,
required this.teamName,
this.clubName,
this.teamId,
this.error,
});
final bool valid;
final String email;
final String teamName;
final String? clubName;
final String? teamId;
final String? error;
factory InvitePreview.fromJson(Map<String, dynamic> json) {
return InvitePreview(
valid: json['valid'] as bool? ?? false,
email: json['email'] as String? ?? '',
teamName: json['team_name'] as String? ?? '',
clubName: json['club_name'] as String?,
teamId: json['team_id'] as String?,
error: json['error'] as String?,
);
}
}

View File

@@ -29,6 +29,7 @@ class Team {
this.staffRole,
this.canStream = true,
this.youtubeMode,
this.canConnectYoutube = false,
});
final String id;
@@ -60,6 +61,7 @@ class Team {
final bool phoneDownloadEnabled;
final bool youtubeEnabled;
final String? youtubeMode;
final bool canConnectYoutube;
bool get canUseYoutube => youtubeEnabled && (premiumFull || youtubeMode == 'matchlivetv_light');
@@ -105,6 +107,7 @@ class Team {
phoneDownloadEnabled: json['phone_download_enabled'] as bool? ?? false,
youtubeEnabled: json['youtube_enabled'] as bool? ?? false,
youtubeMode: json['youtube_mode'] as String?,
canConnectYoutube: json['can_connect_youtube'] as bool? ?? false,
);
}
}