unified_game_services_epic 0.1.0 copy "unified_game_services_epic: ^0.1.0" to clipboard
unified_game_services_epic: ^0.1.0 copied to clipboard

Epic Online Services (EOS) provider for unified_game_services, backed by the EOS C SDK via pure-Dart dart:ffi. Desktop only.

example/lib/main.dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:unified_game_services_epic/unified_game_services_epic.dart';
import 'package:unified_game_services_platform_interface/unified_game_services_platform_interface.dart';

void main() {
  runApp(const EosExampleApp());
}

class EosExampleApp extends StatelessWidget {
  const EosExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'EOS Flutter Test',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF00D4FF),
          surface: Color(0xFF1A1A2E),
        ),
        scaffoldBackgroundColor: const Color(0xFF0F0F1A),
        cardTheme: CardThemeData(
          color: const Color(0xFF1A1A2E),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          elevation: 4,
        ),
      ),
      home: const EosHomeScreen(),
    );
  }
}

class EosHomeScreen extends StatefulWidget {
  const EosHomeScreen({super.key});

  @override
  State<EosHomeScreen> createState() => _EosHomeScreenState();
}

enum _AppState { idle, signingIn, ready, error }

class _EosHomeScreenState extends State<EosHomeScreen> {
  EpicProvider? _provider;
  _AppState _appState = _AppState.idle;
  PlayerProfile? _player;
  String? _errorMessage;

  List<Achievement> _achievements = [];
  bool _loadingAchievements = false;

  List<PlayerProfile> _friends = [];
  bool _loadingFriends = false;
  String? _friendsError;

  // ── Campos del formulario (prefill desde env / --dart-define si existe) ─────
  late final _fields = <String, TextEditingController>{
    'EOS_PRODUCT_ID': TextEditingController(text: _envOrNull('EOS_PRODUCT_ID')),
    'EOS_SANDBOX_ID': TextEditingController(text: _envOrNull('EOS_SANDBOX_ID')),
    'EOS_DEPLOYMENT_ID': TextEditingController(
      text: _envOrNull('EOS_DEPLOYMENT_ID'),
    ),
    'EOS_CLIENT_ID': TextEditingController(text: _envOrNull('EOS_CLIENT_ID')),
    'EOS_CLIENT_SECRET': TextEditingController(
      text: _envOrNull('EOS_CLIENT_SECRET'),
    ),
    'EOS_DEV_AUTH_HOST': TextEditingController(
      text: _envOrNull('EOS_DEV_AUTH_HOST') ?? 'localhost:20304',
    ),
    'EOS_DEV_AUTH_CRED': TextEditingController(
      text: _envOrNull('EOS_DEV_AUTH_CRED'),
    ),
    'EOS_SDK_DIR': TextEditingController(text: _envOrNull('EOS_SDK_DIR')),
  };

  String _field(String key) => _fields[key]!.text.trim();
  String? _fieldOrNull(String key) {
    final v = _field(key);
    return v.isEmpty ? null : v;
  }

  // ── SDK path ──────────────────────────────────────────────────────────────
  String? _sdkLibraryPath() {
    final sdkDir = _fieldOrNull('EOS_SDK_DIR');
    if (sdkDir == null) return null;
    if (Platform.isMacOS) return '$sdkDir/Bin/libEOSSDK-Mac-Shipping.dylib';
    if (Platform.isWindows) return '$sdkDir/Bin/x64/EOSSDK-Win64-Shipping.dll';
    if (Platform.isLinux) return '$sdkDir/Bin/libEOSSDK-Linux-Shipping.so';
    return null;
  }

  // ── Credentials ───────────────────────────────────────────────────────────
  String? _envOrNull(String key) {
    final fromDefine = String.fromEnvironment(key);
    if (fromDefine.isNotEmpty) return fromDefine;
    return Platform.environment[key];
  }

  // ── Sign in ───────────────────────────────────────────────────────────────
  Future<void> _signIn() async {
    setState(() {
      _appState = _AppState.signingIn;
      _errorMessage = null;
    });

    try {
      _provider ??= EpicProvider(
        debugLogging: true,
        credentials: EpicCredentials(
          productId: _field('EOS_PRODUCT_ID'),
          sandboxId: _field('EOS_SANDBOX_ID'),
          deploymentId: _field('EOS_DEPLOYMENT_ID'),
          clientId: _field('EOS_CLIENT_ID'),
          clientSecret: _field('EOS_CLIENT_SECRET'),
        ),
        // Developer Auth Tool → real EAS login (profile + friends) with no
        // launcher. Leave these fields empty to fall back to anonymous Device
        // ID login.
        devAuthHost: _fieldOrNull('EOS_DEV_AUTH_HOST'),
        devAuthCredentialName: _fieldOrNull('EOS_DEV_AUTH_CRED'),
        libraryPath: _sdkLibraryPath(),
        achievementIds: {'TEST': 'TEST_ACHIEVEMENT'},
      );

      final player = await _provider!.signIn();
      setState(() {
        _player = player;
        _appState = _AppState.ready;
      });

      await _loadAchievements();
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
        _appState = _AppState.error;
      });
    }
  }

  // ── Load achievements ─────────────────────────────────────────────────────
  Future<void> _loadAchievements() async {
    if (_provider == null) return;
    setState(() => _loadingAchievements = true);
    try {
      final list = await _provider!.getAchievements();
      setState(() => _achievements = list);
    } catch (e) {
      debugPrint('Error loading achievements: $e');
    } finally {
      setState(() => _loadingAchievements = false);
    }
  }

  // ── Load friends (EAS only) ───────────────────────────────────────────────
  Future<void> _loadFriends() async {
    if (_provider == null) return;
    setState(() {
      _loadingFriends = true;
      _friendsError = null;
    });
    try {
      final list = await _provider!.getFriends();
      setState(() => _friends = list);
    } catch (e) {
      setState(() => _friendsError = e.toString());
    } finally {
      setState(() => _loadingFriends = false);
    }
  }

  // ── Unlock achievement ────────────────────────────────────────────────────
  Future<void> _unlock(Achievement achievement) async {
    if (_provider == null || achievement.isUnlocked) return;
    try {
      await _provider!.unlockAchievement(achievement.id);
      await _loadAchievements(); // Refresh list
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('🏆 "${achievement.title}" unlocked!'),
            backgroundColor: const Color(0xFF00D4FF).withValues(alpha: 0.9),
            behavior: SnackBarBehavior.floating,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(8),
            ),
          ),
        );
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('❌ Error: $e'),
            backgroundColor: Colors.red.shade800,
          ),
        );
      }
    }
  }

  @override
  void dispose() {
    for (final c in _fields.values) {
      c.dispose();
    }
    _provider?.dispose();
    super.dispose();
  }

  // ── Build ─────────────────────────────────────────────────────────────────
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: switch (_appState) {
          _AppState.idle => _buildIdle(),
          _AppState.signingIn => _buildLoading('Signing in to Epic Games...'),
          _AppState.ready => _buildDashboard(),
          _AppState.error => _buildError(),
        },
      ),
    );
  }

  // ── Idle screen ───────────────────────────────────────────────────────────
  Widget _buildIdle() {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 520),
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const SizedBox(height: 16),
              const Icon(
                Icons.sports_esports,
                size: 64,
                color: Color(0xFF00D4FF),
              ),
              const SizedBox(height: 12),
              const Text(
                'EOS Flutter Test',
                textAlign: TextAlign.center,
                style: TextStyle(
                  fontSize: 26,
                  fontWeight: FontWeight.bold,
                  letterSpacing: 1.2,
                ),
              ),
              const SizedBox(height: 24),
              const Text(
                'Credentials (Dev Portal)',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 8),
              _formField('EOS_PRODUCT_ID', 'Product ID'),
              _formField('EOS_SANDBOX_ID', 'Sandbox ID'),
              _formField('EOS_DEPLOYMENT_ID', 'Deployment ID'),
              _formField('EOS_CLIENT_ID', 'Client ID'),
              _formField('EOS_CLIENT_SECRET', 'Client Secret', obscure: true),
              const SizedBox(height: 16),
              const Text(
                'Developer Auth Tool (real EAS login)',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 4),
              const Text(
                'Empty → anonymous Device ID login (no real profile or friends).',
                style: TextStyle(color: Colors.white38, fontSize: 11),
              ),
              const SizedBox(height: 8),
              _formField('EOS_DEV_AUTH_HOST', 'Host (e.g. 127.0.0.1:20304)'),
              _formField('EOS_DEV_AUTH_CRED', 'Credential name'),
              const SizedBox(height: 16),
              const Text('SDK', style: TextStyle(fontWeight: FontWeight.bold)),
              const SizedBox(height: 8),
              _formField(
                'EOS_SDK_DIR',
                'EOS_SDK_DIR path (empty = dylib on PATH/rpath)',
              ),
              const SizedBox(height: 24),
              FilledButton.icon(
                onPressed: _signIn,
                icon: const Icon(Icons.login),
                label: const Text('Sign In'),
                style: FilledButton.styleFrom(
                  padding: const EdgeInsets.symmetric(vertical: 16),
                  textStyle: const TextStyle(fontSize: 16),
                ),
              ),
              const SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }

  Widget _formField(String key, String label, {bool obscure = false}) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: TextField(
        controller: _fields[key],
        obscureText: obscure,
        autocorrect: false,
        enableSuggestions: false,
        style: const TextStyle(fontFamily: 'monospace', fontSize: 13),
        decoration: InputDecoration(
          labelText: label,
          isDense: true,
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }

  // ── Loading screen ────────────────────────────────────────────────────────
  Widget _buildLoading(String message) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const CircularProgressIndicator(color: Color(0xFF00D4FF)),
          const SizedBox(height: 24),
          Text(
            message,
            style: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
          ),
        ],
      ),
    );
  }

  // ── Error screen ──────────────────────────────────────────────────────────
  Widget _buildError() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.redAccent),
            const SizedBox(height: 16),
            const Text(
              'Sign-in error',
              style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 12),
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.red.shade900.withValues(alpha: 0.3),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Text(
                _errorMessage ?? '',
                style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
            ),
            const SizedBox(height: 24),
            OutlinedButton.icon(
              onPressed: () => setState(() => _appState = _AppState.idle),
              icon: const Icon(Icons.refresh),
              label: const Text('Retry'),
            ),
          ],
        ),
      ),
    );
  }

  // ── Dashboard ─────────────────────────────────────────────────────────────
  Widget _buildDashboard() {
    final unlocked = _achievements.where((a) => a.isUnlocked).length;
    final total = _achievements.length;
    final progress = total > 0 ? unlocked / total : 0.0;

    return CustomScrollView(
      slivers: [
        // App bar con info del jugador
        SliverAppBar(
          expandedHeight: 160,
          pinned: true,
          backgroundColor: const Color(0xFF0F0F1A),
          flexibleSpace: FlexibleSpaceBar(
            background: _buildPlayerHeader(unlocked, total, progress),
          ),
          actions: [
            IconButton(
              tooltip: 'Reload achievements',
              onPressed: _loadingAchievements ? null : _loadAchievements,
              icon: _loadingAchievements
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.refresh),
            ),
          ],
        ),

        // Panel de amigos (EAS)
        SliverToBoxAdapter(child: _buildFriendsPanel()),

        // Achievement list
        if (_loadingAchievements && _achievements.isEmpty)
          const SliverFillRemaining(
            child: Center(
              child: CircularProgressIndicator(color: Color(0xFF00D4FF)),
            ),
          )
        else if (_achievements.isEmpty)
          SliverFillRemaining(
            child: Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(
                    Icons.emoji_events_outlined,
                    size: 64,
                    color: Colors.white.withValues(alpha: 0.3),
                  ),
                  const SizedBox(height: 12),
                  Text(
                    'No achievements available',
                    style: TextStyle(
                      color: Colors.white.withValues(alpha: 0.5),
                    ),
                  ),
                ],
              ),
            ),
          )
        else
          SliverPadding(
            padding: const EdgeInsets.all(16),
            sliver: SliverList.separated(
              itemCount: _achievements.length,
              separatorBuilder: (_, __) => const SizedBox(height: 10),
              itemBuilder: (context, i) => _AchievementCard(
                achievement: _achievements[i],
                onUnlock: () => _unlock(_achievements[i]),
              ),
            ),
          ),
      ],
    );
  }

  Widget _buildFriendsPanel() {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              const Icon(Icons.people_alt, color: Color(0xFF00D4FF), size: 20),
              const SizedBox(width: 8),
              const Text(
                'Epic Friends',
                style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
              ),
              const Spacer(),
              FilledButton.tonalIcon(
                onPressed: _loadingFriends ? null : _loadFriends,
                icon: _loadingFriends
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.download, size: 18),
                label: const Text('Load'),
              ),
            ],
          ),
          if (_friendsError != null)
            Padding(
              padding: const EdgeInsets.only(top: 8),
              child: Text(
                _friendsError!,
                style: const TextStyle(
                  color: Colors.redAccent,
                  fontSize: 12,
                  fontFamily: 'monospace',
                ),
              ),
            )
          else if (_friends.isEmpty && !_loadingFriends)
            const Padding(
              padding: EdgeInsets.symmetric(vertical: 8),
              child: Text(
                'No friends loaded.',
                style: TextStyle(color: Colors.white38, fontSize: 12),
              ),
            )
          else
            ..._friends.map(
              (f) => ListTile(
                contentPadding: EdgeInsets.zero,
                dense: true,
                leading: const CircleAvatar(
                  radius: 16,
                  backgroundColor: Color(0x3300D4FF),
                  child: Icon(Icons.person, size: 18, color: Color(0xFF00D4FF)),
                ),
                title: Text(f.displayName),
                subtitle: Text(
                  f.id,
                  style: const TextStyle(fontFamily: 'monospace', fontSize: 10),
                ),
              ),
            ),
          const Divider(height: 24),
        ],
      ),
    );
  }

  Widget _buildPlayerHeader(int unlocked, int total, double progress) {
    return Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFF1A1A2E), Color(0xFF16213E)],
        ),
      ),
      padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          Row(
            children: [
              CircleAvatar(
                radius: 24,
                backgroundColor: const Color(0xFF00D4FF).withValues(alpha: 0.2),
                child: const Icon(
                  Icons.person,
                  color: Color(0xFF00D4FF),
                  size: 28,
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      _player?.displayName ?? 'Connected player',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      _player != null
                          ? '${_player!.id.substring(0, 8)}...${_player!.id.substring(_player!.id.length - 6)}'
                          : '—',
                      style: const TextStyle(
                        fontFamily: 'monospace',
                        fontSize: 12,
                        color: Color(0xFF00D4FF),
                      ),
                    ),
                  ],
                ),
              ),
              _StatBadge(label: 'Logros', value: '$unlocked / $total'),
            ],
          ),
          const SizedBox(height: 12),
          ClipRRect(
            borderRadius: BorderRadius.circular(4),
            child: LinearProgressIndicator(
              value: progress,
              minHeight: 6,
              backgroundColor: Colors.white12,
              valueColor: const AlwaysStoppedAnimation(Color(0xFF00D4FF)),
            ),
          ),
          const SizedBox(height: 4),
          Text(
            '${(progress * 100).toStringAsFixed(0)}% completed',
            style: const TextStyle(color: Colors.white38, fontSize: 11),
          ),
        ],
      ),
    );
  }
}

// ── Achievement card ──────────────────────────────────────────────────────────
class _AchievementCard extends StatelessWidget {
  const _AchievementCard({required this.achievement, required this.onUnlock});

  final Achievement achievement;
  final VoidCallback onUnlock;

  @override
  Widget build(BuildContext context) {
    final unlocked = achievement.isUnlocked;
    final accent = unlocked ? const Color(0xFFFFD700) : Colors.white30;

    return Card(
      child: InkWell(
        onTap: unlocked ? null : onUnlock,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              // Icon / trophy
              Container(
                width: 52,
                height: 52,
                decoration: BoxDecoration(
                  color: accent.withValues(alpha: 0.1),
                  shape: BoxShape.circle,
                  border: Border.all(
                    color: accent.withValues(alpha: 0.4),
                    width: 1.5,
                  ),
                ),
                child: achievement.iconUrl != null
                    ? Image.network(achievement.iconUrl!)
                    : Icon(
                        unlocked ? Icons.emoji_events : Icons.lock_outline,
                        color: accent,
                        size: 26,
                      ),
              ),
              const SizedBox(width: 16),

              // Name + description
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      achievement.title,
                      style: TextStyle(
                        fontWeight: FontWeight.bold,
                        fontSize: 15,
                        color: unlocked ? Colors.white : Colors.white70,
                      ),
                    ),
                    if (achievement.description != null) ...[
                      const SizedBox(height: 4),
                      Text(
                        achievement.description ?? 'No description',
                        style: const TextStyle(
                          color: Colors.white38,
                          fontSize: 12,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                    if (achievement.unlockedAt != null) ...[
                      const SizedBox(height: 4),
                      Text(
                        achievement.unlockedAt!.toUtc().toString(),
                        style: const TextStyle(
                          color: Colors.white38,
                          fontSize: 10,
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ],
                ),
              ),
              const SizedBox(width: 12),

              // Status badge
              _StatusChip(unlocked: unlocked),
            ],
          ),
        ),
      ),
    );
  }
}

// ── Chips y badges ────────────────────────────────────────────────────────────
class _StatusChip extends StatelessWidget {
  const _StatusChip({required this.unlocked});
  final bool unlocked;

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
      decoration: BoxDecoration(
        color: unlocked
            ? const Color(0xFFFFD700).withValues(alpha: 0.15)
            : Colors.white.withValues(alpha: 0.05),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(
          color: unlocked
              ? const Color(0xFFFFD700).withValues(alpha: 0.5)
              : Colors.white12,
        ),
      ),
      child: Text(
        unlocked ? '✓ Unlocked' : 'Locked',
        style: TextStyle(
          fontSize: 11,
          fontWeight: FontWeight.w600,
          color: unlocked ? const Color(0xFFFFD700) : Colors.white38,
        ),
      ),
    );
  }
}

class _StatBadge extends StatelessWidget {
  const _StatBadge({required this.label, required this.value});
  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.end,
      children: [
        Text(
          value,
          style: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
            color: Color(0xFF00D4FF),
          ),
        ),
        Text(
          label,
          style: const TextStyle(color: Colors.white38, fontSize: 11),
        ),
      ],
    );
  }
}
0
likes
160
points
10
downloads

Documentation

API reference

Publisher

verified publisheraprimo.dev

Weekly Downloads

Epic Online Services (EOS) provider for unified_game_services, backed by the EOS C SDK via pure-Dart dart:ffi. Desktop only.

Repository (GitHub)
View/report issues

Topics

#game-services #achievements #leaderboards #epic-games #gamedev

License

MIT (license)

Dependencies

ffi, unified_game_services_platform_interface

More

Packages that depend on unified_game_services_epic