final class LauncherGameTicket { static final RegExp _accountPattern = RegExp(r'^[A-Z0-9]{3,16}$'); static final RegExp _ticketPattern = RegExp(r'^[A-Z0-9]{16}$'); final String account; final String ticket; final DateTime expiresAt; const LauncherGameTicket({ required this.account, required this.ticket, required this.expiresAt, }); factory LauncherGameTicket.fromJson(Map json) { final rawAccount = json['account']; final rawTicket = json['ticket']; final rawExpiresAt = json['expires_at']; if (rawAccount is! String || rawTicket is! String || rawExpiresAt is! String) { throw const FormatException('Invalid game ticket payload types.'); } final account = rawAccount; final ticket = rawTicket; final expiresAt = DateTime.tryParse(rawExpiresAt); if (!_accountPattern.hasMatch(account)) { throw const FormatException('Invalid game ticket account.'); } if (!_ticketPattern.hasMatch(ticket)) { throw const FormatException('Invalid game ticket value.'); } if (expiresAt == null) { throw const FormatException('Invalid game ticket expiration.'); } return LauncherGameTicket( account: account, ticket: ticket, expiresAt: expiresAt.toUtc(), ); } bool isUsable({ DateTime? now, Duration minimumValidity = const Duration(seconds: 5), }) { final currentTime = (now ?? DateTime.now()).toUtc(); return expiresAt.isAfter(currentTime.add(minimumValidity)); } }