153 lines
4.9 KiB
Dart
153 lines
4.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
import '../../shared/app_toast.dart';
|
|
import 'member_provider.dart';
|
|
|
|
class PayArgs {
|
|
final String orderNo;
|
|
final String payUrl;
|
|
|
|
const PayArgs({required this.orderNo, required this.payUrl});
|
|
}
|
|
|
|
enum PayPhase { launching, paying, paid, timeout, failed }
|
|
|
|
/// 支付页:打开系统浏览器收银台,2s 轮询订单状态(上限 60s)
|
|
class PayPage extends ConsumerStatefulWidget {
|
|
final PayArgs args;
|
|
|
|
const PayPage({super.key, required this.args});
|
|
|
|
@override
|
|
ConsumerState<PayPage> createState() => _PayPageState();
|
|
}
|
|
|
|
class _PayPageState extends ConsumerState<PayPage> {
|
|
PayPhase _phase = PayPhase.launching;
|
|
Timer? _timer;
|
|
int _elapsed = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_start();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _start() async {
|
|
try {
|
|
final ok = await launchUrl(Uri.parse(widget.args.payUrl),
|
|
mode: LaunchMode.externalApplication);
|
|
if (!ok) {
|
|
setState(() => _phase = PayPhase.failed);
|
|
return;
|
|
}
|
|
setState(() => _phase = PayPhase.paying);
|
|
} catch (e) {
|
|
setState(() => _phase = PayPhase.failed);
|
|
return;
|
|
}
|
|
_timer = Timer.periodic(const Duration(seconds: 2), (_) => _check());
|
|
}
|
|
|
|
Future<void> _check() async {
|
|
_elapsed += 2;
|
|
try {
|
|
final status = await fetchOrderStatus(ref, widget.args.orderNo);
|
|
if (status == 'paid') {
|
|
_timer?.cancel();
|
|
if (!mounted) return;
|
|
setState(() => _phase = PayPhase.paid);
|
|
showToast('会员开通成功');
|
|
return;
|
|
}
|
|
if (_elapsed >= 60) {
|
|
_timer?.cancel();
|
|
if (!mounted) return;
|
|
setState(() => _phase = PayPhase.timeout);
|
|
}
|
|
} catch (_) {
|
|
// 轮询失败不中断,下次再试
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('会员支付')),
|
|
body: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
switch (_phase) {
|
|
PayPhase.launching ||
|
|
PayPhase.paying => Column(children: [
|
|
const CircularProgressIndicator(),
|
|
const SizedBox(height: 16),
|
|
const Text('请在浏览器中完成支付,正在确认结果…'),
|
|
const SizedBox(height: 8),
|
|
Text('订单号 ${widget.args.orderNo}',
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton(
|
|
onPressed: () => _check(),
|
|
child: const Text('我已完成支付'),
|
|
),
|
|
]),
|
|
PayPhase.paid => Column(children: [
|
|
Icon(Icons.check_circle, size: 64, color: scheme.primary),
|
|
const SizedBox(height: 12),
|
|
const Text('支付成功,会员已开通!',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 16),
|
|
FilledButton(
|
|
onPressed: () => context.pop(),
|
|
child: const Text('返回会员中心'),
|
|
),
|
|
]),
|
|
PayPhase.timeout => Column(children: [
|
|
Icon(Icons.hourglass_empty, size: 64, color: Colors.orange),
|
|
const SizedBox(height: 12),
|
|
const Text('支付结果确认中'),
|
|
const SizedBox(height: 8),
|
|
const Text('可稍后到会员中心查看开通状态,以支付结果为准',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.grey, fontSize: 12)),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton(
|
|
onPressed: () => context.pop(),
|
|
child: const Text('返回'),
|
|
),
|
|
]),
|
|
PayPhase.failed => Column(children: [
|
|
Icon(Icons.error_outline, size: 64, color: scheme.error),
|
|
const SizedBox(height: 12),
|
|
const Text('无法打开支付页面'),
|
|
const SizedBox(height: 16),
|
|
OutlinedButton(
|
|
onPressed: () => context.pop(),
|
|
child: const Text('返回'),
|
|
),
|
|
]),
|
|
},
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|