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

@@ -0,0 +1,77 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../features/matches/team_providers.dart';
import 'invite_storage.dart';
/// Gestisce link invito (join) e ritorno OAuth YouTube dall'app.
class DeepLinkListener extends ConsumerStatefulWidget {
const DeepLinkListener({super.key, required this.child});
final Widget child;
@override
ConsumerState<DeepLinkListener> createState() => _DeepLinkListenerState();
}
class _DeepLinkListenerState extends ConsumerState<DeepLinkListener> {
final _appLinks = AppLinks();
StreamSubscription<Uri>? _sub;
@override
void initState() {
super.initState();
_handleInitialLink();
_sub = _appLinks.uriLinkStream.listen(_onUri, onError: (_) {});
}
Future<void> _handleInitialLink() async {
final uri = await _appLinks.getInitialLink();
if (uri != null) await _onUri(uri);
}
Future<void> _onUri(Uri uri) async {
final inviteToken = _extractInviteToken(uri);
if (inviteToken != null) {
await InviteStorage.saveToken(inviteToken);
if (!mounted) return;
context.go('/login?invite=$inviteToken');
return;
}
if (uri.scheme == 'matchlivetv' && uri.host == 'youtube-connected') {
ref.invalidate(teamsProvider);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Canale YouTube collegato. Puoi selezionare YouTube Live.'),
duration: Duration(seconds: 4),
),
);
}
}
String? _extractInviteToken(Uri uri) {
if (uri.scheme == 'matchlivetv' && uri.host == 'join') {
final segment = uri.pathSegments.isNotEmpty ? uri.pathSegments.first : uri.path.replaceFirst('/', '');
return segment.isEmpty ? null : segment;
}
if (uri.pathSegments.length >= 2 && uri.pathSegments.first == 'join') {
return uri.pathSegments[1];
}
return null;
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) => widget.child;
}

View File

@@ -0,0 +1,20 @@
import 'package:shared_preferences/shared_preferences.dart';
class InviteStorage {
static const _keyToken = 'pending_invite_token';
static Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyToken, token);
}
static Future<String?> readToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_keyToken);
}
static Future<void> clear() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
}
}