| .. | .. |
|---|
| 5 | 5 | import '../models/server_config.dart'; |
|---|
| 6 | 6 | import '../providers/providers.dart'; |
|---|
| 7 | 7 | import '../services/mqtt_service.dart' show ConnectionStatus; |
|---|
| 8 | +import '../services/purchase_service.dart'; |
|---|
| 8 | 9 | import '../services/wol_service.dart'; |
|---|
| 9 | 10 | import '../theme/app_theme.dart'; |
|---|
| 10 | 11 | import '../widgets/status_dot.dart'; |
|---|
| .. | .. |
|---|
| 25 | 26 | late final TextEditingController _macController; |
|---|
| 26 | 27 | late final TextEditingController _mqttTokenController; |
|---|
| 27 | 28 | bool _isWaking = false; |
|---|
| 29 | + bool _iapLoading = false; |
|---|
| 28 | 30 | |
|---|
| 29 | 31 | @override |
|---|
| 30 | 32 | void initState() { |
|---|
| 31 | 33 | super.initState(); |
|---|
| 34 | + PurchaseService.instance.addListener(_onPurchaseChanged); |
|---|
| 32 | 35 | final config = ref.read(serverConfigProvider); |
|---|
| 33 | 36 | _localHostController = |
|---|
| 34 | 37 | TextEditingController(text: config?.localHost ?? ''); |
|---|
| .. | .. |
|---|
| 46 | 49 | |
|---|
| 47 | 50 | @override |
|---|
| 48 | 51 | void dispose() { |
|---|
| 52 | + PurchaseService.instance.removeListener(_onPurchaseChanged); |
|---|
| 49 | 53 | _localHostController.dispose(); |
|---|
| 50 | 54 | _vpnHostController.dispose(); |
|---|
| 51 | 55 | _remoteHostController.dispose(); |
|---|
| .. | .. |
|---|
| 53 | 57 | _macController.dispose(); |
|---|
| 54 | 58 | _mqttTokenController.dispose(); |
|---|
| 55 | 59 | super.dispose(); |
|---|
| 60 | + } |
|---|
| 61 | + |
|---|
| 62 | + void _onPurchaseChanged() { |
|---|
| 63 | + if (!mounted) return; |
|---|
| 64 | + final isPro = PurchaseService.instance.isPro; |
|---|
| 65 | + ref.read(isProProvider.notifier).state = isPro; |
|---|
| 66 | + setState(() => _iapLoading = PurchaseService.instance.isLoading); |
|---|
| 67 | + final error = PurchaseService.instance.errorMessage; |
|---|
| 68 | + if (error != null && mounted) { |
|---|
| 69 | + ScaffoldMessenger.of(context).showSnackBar( |
|---|
| 70 | + SnackBar(content: Text(error)), |
|---|
| 71 | + ); |
|---|
| 72 | + } |
|---|
| 73 | + if (isPro && mounted) { |
|---|
| 74 | + ScaffoldMessenger.of(context).showSnackBar( |
|---|
| 75 | + const SnackBar( |
|---|
| 76 | + content: Text('PAILot Pro activated. Enjoy unlimited sessions!'), |
|---|
| 77 | + duration: Duration(seconds: 3), |
|---|
| 78 | + ), |
|---|
| 79 | + ); |
|---|
| 80 | + } |
|---|
| 81 | + } |
|---|
| 82 | + |
|---|
| 83 | + Future<void> _handleUpgrade() async { |
|---|
| 84 | + setState(() => _iapLoading = true); |
|---|
| 85 | + await PurchaseService.instance.purchaseFullAccess(); |
|---|
| 86 | + } |
|---|
| 87 | + |
|---|
| 88 | + Future<void> _handleRestore() async { |
|---|
| 89 | + setState(() => _iapLoading = true); |
|---|
| 90 | + await PurchaseService.instance.restorePurchases(); |
|---|
| 91 | + if (mounted) { |
|---|
| 92 | + ScaffoldMessenger.of(context).showSnackBar( |
|---|
| 93 | + const SnackBar( |
|---|
| 94 | + content: Text('Checking for previous purchases...'), |
|---|
| 95 | + duration: Duration(seconds: 2), |
|---|
| 96 | + ), |
|---|
| 97 | + ); |
|---|
| 98 | + } |
|---|
| 56 | 99 | } |
|---|
| 57 | 100 | |
|---|
| 58 | 101 | Future<void> _save() async { |
|---|
| .. | .. |
|---|
| 328 | 371 | icon: const Icon(Icons.shield_outlined), |
|---|
| 329 | 372 | label: const Text('Reset Server Trust'), |
|---|
| 330 | 373 | ), |
|---|
| 374 | + const SizedBox(height: 24), |
|---|
| 375 | + |
|---|
| 376 | + // --- PAILot Pro --- |
|---|
| 377 | + const Divider(), |
|---|
| 378 | + const SizedBox(height: 8), |
|---|
| 379 | + Row( |
|---|
| 380 | + children: [ |
|---|
| 381 | + const Icon(Icons.star, size: 18), |
|---|
| 382 | + const SizedBox(width: 8), |
|---|
| 383 | + Text( |
|---|
| 384 | + 'PAILot Pro', |
|---|
| 385 | + style: Theme.of(context).textTheme.titleMedium, |
|---|
| 386 | + ), |
|---|
| 387 | + const Spacer(), |
|---|
| 388 | + Consumer( |
|---|
| 389 | + builder: (ctx, ref, _) { |
|---|
| 390 | + final isPro = ref.watch(isProProvider); |
|---|
| 391 | + return Chip( |
|---|
| 392 | + label: Text( |
|---|
| 393 | + isPro ? 'Active' : 'Free Tier', |
|---|
| 394 | + style: TextStyle( |
|---|
| 395 | + fontSize: 11, |
|---|
| 396 | + color: isPro ? Colors.white : null, |
|---|
| 397 | + ), |
|---|
| 398 | + ), |
|---|
| 399 | + backgroundColor: isPro ? AppColors.accent : null, |
|---|
| 400 | + padding: EdgeInsets.zero, |
|---|
| 401 | + visualDensity: VisualDensity.compact, |
|---|
| 402 | + ); |
|---|
| 403 | + }, |
|---|
| 404 | + ), |
|---|
| 405 | + ], |
|---|
| 406 | + ), |
|---|
| 407 | + const SizedBox(height: 4), |
|---|
| 408 | + Text( |
|---|
| 409 | + 'Free: 2 sessions, messages expire after 15 min\n' |
|---|
| 410 | + 'Pro: unlimited sessions, messages persist forever', |
|---|
| 411 | + style: Theme.of(context) |
|---|
| 412 | + .textTheme |
|---|
| 413 | + .bodySmall |
|---|
| 414 | + ?.copyWith(color: Colors.grey.shade500), |
|---|
| 415 | + ), |
|---|
| 416 | + const SizedBox(height: 12), |
|---|
| 417 | + Consumer( |
|---|
| 418 | + builder: (ctx, ref, _) { |
|---|
| 419 | + final isPro = ref.watch(isProProvider); |
|---|
| 420 | + if (isPro) { |
|---|
| 421 | + return const Center( |
|---|
| 422 | + child: Text( |
|---|
| 423 | + 'Full access active', |
|---|
| 424 | + style: TextStyle(color: AppColors.accent), |
|---|
| 425 | + ), |
|---|
| 426 | + ); |
|---|
| 427 | + } |
|---|
| 428 | + return Column( |
|---|
| 429 | + crossAxisAlignment: CrossAxisAlignment.stretch, |
|---|
| 430 | + children: [ |
|---|
| 431 | + ElevatedButton.icon( |
|---|
| 432 | + onPressed: _iapLoading ? null : _handleUpgrade, |
|---|
| 433 | + icon: _iapLoading |
|---|
| 434 | + ? const SizedBox( |
|---|
| 435 | + width: 16, |
|---|
| 436 | + height: 16, |
|---|
| 437 | + child: |
|---|
| 438 | + CircularProgressIndicator(strokeWidth: 2), |
|---|
| 439 | + ) |
|---|
| 440 | + : const Icon(Icons.upgrade), |
|---|
| 441 | + label: const Text('Upgrade to Pro — \$4.99'), |
|---|
| 442 | + style: ElevatedButton.styleFrom( |
|---|
| 443 | + backgroundColor: AppColors.accent, |
|---|
| 444 | + foregroundColor: Colors.white, |
|---|
| 445 | + ), |
|---|
| 446 | + ), |
|---|
| 447 | + const SizedBox(height: 8), |
|---|
| 448 | + OutlinedButton.icon( |
|---|
| 449 | + onPressed: _iapLoading ? null : _handleRestore, |
|---|
| 450 | + icon: const Icon(Icons.restore), |
|---|
| 451 | + label: const Text('Restore Purchase'), |
|---|
| 452 | + ), |
|---|
| 453 | + ], |
|---|
| 454 | + ); |
|---|
| 455 | + }, |
|---|
| 456 | + ), |
|---|
| 331 | 457 | const SizedBox(height: 12), |
|---|
| 332 | 458 | ], |
|---|
| 333 | 459 | ), |
|---|