import 'dart:async'; import 'package:flutter/foundation.dart'; import '../../core/data/backend.dart'; import 'galaxy_data.dart'; import '../../core/i18n/app_locale.dart'; import '../../core/ui/fx_intensity.dart'; /// HOME과 Galaxy의 동일한 기여 거래: 연타 합산, 확정 결과, 동일 키 재확인. class ZapController extends ChangeNotifier { ZapController(this.repository, {required this.onFailure}); final GalaxyRepository repository; final ValueChanged onFailure; int? balance; int pending = 0; bool sending = false; int touchSerial = 0; ZapResult? result; Timer? _burst; bool _disposed = false; ({String fandomId, int count, String key})? _batch; bool get needsRetry => _batch != null && !sending; /// 지금 결과가 ZAP 이 아니라 퀘스트 보상 확정의 행성 반응인가([react]). bool questReaction = false; /// 퀘스트 보상 확정 뒤 행성 반응 — v1.5 4장 「성공 햅틱 → 보상/기여 에너지 이동 → Planet 반응 → /// 실제 지급/성장 결과」의 에너지 이동 · 반응 두 단계. ZAP 과 같은 이동 · 흡수 연출을 쓰고 잔액은 /// 건드리지 않는다. 서버가 보상을 확정한 뒤에만 부른다(C-79). Future react() async { if (_disposed || sending || pending > 0) return; questReaction = true; touchSerial++; notifyListeners(); // 에너지가 행성에 닿은 뒤 흡수 — ZapFeedback 이동 시간(380ms)과 같다. await Future.delayed(const Duration(milliseconds: 380)); if (_disposed || !questReaction) return; result = ZapResult( txId: 'quest-${DateTime.now().microsecondsSinceEpoch}', balanceAfter: -1, ); notifyListeners(); } void tap(String fandomId) { if (_disposed || sending || _batch != null || fandomId.isEmpty) return; questReaction = false; if (pending == 0) fxHaptic(FxIntensity.small); pending++; touchSerial++; notifyListeners(); _burst ??= Timer(const Duration(milliseconds: 450), () => _flush(fandomId)); } void retry() { if (_batch case final batch?) _flush(batch.fandomId); } Future _flush(String fandomId) async { if (_disposed || sending) return; final batch = _batch ?? ( fandomId: fandomId, count: pending, key: 'z${DateTime.now().microsecondsSinceEpoch}', ); if (batch.count == 0) return; _burst?.cancel(); _burst = null; _batch = batch; sending = true; pending = 0; notifyListeners(); try { final receipt = await repository.zap( fandomId: batch.fandomId, count: batch.count, clientSeq: batch.key, ); if (_disposed) return; if (receipt.txId.isEmpty) throw StateError('Missing ZAP receipt'); if (receipt.balanceAfter >= 0) balance = receipt.balanceAfter; result = receipt; _batch = null; } catch (error) { if (_disposed) return; onFailure(switch (error) { BackendException(code: 'E-POP-01') => appL10n.zapFailNoPop, BackendException(code: 'E-POP-02') => appL10n.zapFailLedger, BackendException(code: 'E-AUTH-03') => appL10n.authLoginRequired, _ => appL10n.zapFailRetry, }); } finally { if (!_disposed) { sending = false; notifyListeners(); } } } @override void dispose() { _disposed = true; _burst?.cancel(); super.dispose(); } }