Pārlūkot izejas kodu

fix(alipay): add AppState polling fallback to fix TestFlight callback interruption

zbb 1 mēnesi atpakaļ
vecāks
revīzija
42d5ba64f4

+ 1 - 1
app.json

@@ -12,7 +12,7 @@
       "supportsTablet": false,
       "bundleIdentifier": "com.asios",
       "appleTeamId": "Y9ZVX3FRX6",
-      "buildNumber": "8",
+      "buildNumber": "9",
       "infoPlist": {
         "ITSAppUsesNonExemptEncryption": false,
         "NSPhotoLibraryUsageDescription": "App需要访问您的相册以便您可以上传头像或保存商品分享图片。",

+ 51 - 4
app/award-detail/components/CheckoutModal.tsx

@@ -9,6 +9,7 @@ import React, {
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     Dimensions,
     Modal,
     ScrollView,
@@ -274,28 +275,74 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       tradeNo: string,
     ) => {
       if (type === "ALIPAY" || type.includes("ALIPAY")) {
+        let appStateSub: any = null;
+        let isResolved = false;
+
         try {
-          // 设置支付宝 URL Scheme(用于支付完成后返回APP)
           Alipay.setAlipayScheme("alipay2021005175632205");
 
-          // 使用 expo-native-alipay 调用支付宝
+          // Watch for app returning to foreground as a fallback
+          appStateSub = AppState.addEventListener(
+            "change",
+            async (nextAppState) => {
+              if (nextAppState === "active" && !isResolved) {
+                // Wait a bit to give the SDK a chance to resolve natively first
+                setTimeout(async () => {
+                  if (!isResolved) {
+                    console.log(
+                      "Alipay SDK did not resolve, checking server manually...",
+                    );
+                    try {
+                      const res = await getApplyResult(tradeNo);
+                      if (
+                        res?.paySuccess ||
+                        (res?.inventoryList && res.inventoryList.length > 0)
+                      ) {
+                        isResolved = true;
+                        handleSuccess(tradeNo);
+                      }
+                    } catch (e) {
+                      console.log("Fallback check failed", e);
+                    }
+                  }
+                }, 3000);
+              }
+            },
+          );
+
           const result = await Alipay.pay(payInfo);
+          isResolved = true;
           console.log("Alipay Result:", result);
 
-          // resultStatus: '9000' 表示支付成功
           const status = result?.resultStatus;
 
           if (status === "9000") {
-            // 支付成功,跳转到抽奖页面
             handleSuccess(tradeNo);
           } else if (status === "6001") {
             Alert.alert("提示", "用户取消支付");
           } else {
+            // Also poll server just in case the status is wrong but it succeeded
+            try {
+              const res = await getApplyResult(tradeNo);
+              if (
+                res?.paySuccess ||
+                (res?.inventoryList && res.inventoryList.length > 0)
+              ) {
+                handleSuccess(tradeNo);
+                return;
+              }
+            } catch (ignore) {}
+
             Alert.alert("支付中断", `状态码: ${status}`);
           }
         } catch (e: any) {
+          isResolved = true;
           console.log("Alipay Error:", e);
           Alert.alert("支付异常", e.message || "调用支付宝失败");
+        } finally {
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
       } else {
         Alert.alert("提示", "微信支付暂未实现");

+ 44 - 9
app/purse/recharge.tsx

@@ -4,6 +4,7 @@ import React, { useState } from "react";
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     StyleSheet,
     Text,
     TextInput,
@@ -65,15 +66,49 @@ export default function RechargeScreen() {
         (payInfo.startsWith("alipay_root_cert_sn") ||
           payInfo.includes("alipay_sdk"))
       ) {
-        Alipay.setAlipayScheme("alipay2021005175632205");
-        const result = await Alipay.pay(payInfo);
-        console.log("Alipay Result:", result);
-
-        if (result?.resultStatus === "9000") {
-          Alert.alert("提示", "充值成功");
-          router.replace("/wallet/recharge_record");
-        } else {
-          Alert.alert("提示", "支付未完成");
+        let appStateSub: any = null;
+        let isResolved = false;
+
+        try {
+          Alipay.setAlipayScheme("alipay2021005175632205");
+
+          appStateSub = AppState.addEventListener(
+            "change",
+            async (nextAppState) => {
+              if (nextAppState === "active" && !isResolved) {
+                setTimeout(async () => {
+                  if (!isResolved) {
+                    console.log(
+                      "Alipay SDK did not resolve natively, assuming success to refresh...",
+                    );
+                    isResolved = true;
+                    Alert.alert("提示", "充值已发起,请稍后查看余额");
+                    router.replace("/wallet/recharge_record");
+                  }
+                }, 3000);
+              }
+            },
+          );
+
+          const result = await Alipay.pay(payInfo);
+          isResolved = true;
+          console.log("Alipay Result:", result);
+
+          if (result?.resultStatus === "9000") {
+            Alert.alert("提示", "充值成功");
+            router.replace("/wallet/recharge_record");
+          } else if (result?.resultStatus === "6001") {
+            Alert.alert("提示", "用户取消支付");
+          } else {
+            Alert.alert("提示", "支付未完成");
+          }
+        } catch (e: any) {
+          isResolved = true;
+          Alert.alert("提示", "调用支付宝失败: " + (e.message || ""));
+        } finally {
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
       } else if (res?.data?.tradeNo && !payInfo) {
         Alert.alert("提示", "获取支付信息失败(仅获取到订单号)");

+ 29 - 34
app/safepay.tsx

@@ -1,13 +1,7 @@
 import { Colors } from "@/constants/Colors";
 import { useLocalSearchParams, useRouter } from "expo-router";
 import { useEffect } from "react";
-import {
-    ActivityIndicator,
-    AppState,
-    StyleSheet,
-    Text,
-    View,
-} from "react-native";
+import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
 
 /**
  * Alipay Callback Handler
@@ -21,40 +15,33 @@ export default function AlipayResult() {
   useEffect(() => {
     console.log("Alipay callback params:", params);
 
-    let hasNavigated = false;
-    let fallbackTimer: ReturnType<typeof setTimeout>;
+    // In TestFlight/Release builds, performing a router.back() or router.dismiss() here
+    // forces a React Navigation transition that often unmounts the underlying component
+    // (CheckoutModal) BEFORE the native `Alipay.pay()` Promise has time to resolve and
+    // trigger `handleSuccess`.
 
-    const navigateBack = () => {
-      if (hasNavigated) return;
-      hasNavigated = true;
-      if (router.canGoBack()) {
-        router.back();
-      } else {
-        router.replace("/");
-      }
-    };
+    // Instead of forcing a navigation here, we simply act as a passive sink for the deep link
+    // to prevent the "Unmatched Route" warning. We trust the original `Alipay.pay()`
+    // call in CheckoutModal to receive its native completion event and navigate the user
+    // to the result screen (e.g., /happy-spin) which will naturally replace this route.
 
-    // Listen for AppState changes to know when user actually returns to the app
-    const subscription = AppState.addEventListener("change", (nextAppState) => {
-      if (nextAppState === "active") {
-        // App has returned to foreground. Give a tiny delay for React Native to settle.
-        setTimeout(navigateBack, 500);
-      }
-    });
-
-    // Fallback: if AppState doesn't trigger for some reason, navigate after a max delay (e.g., 2000ms)
-    fallbackTimer = setTimeout(navigateBack, 2000);
-
-    return () => {
-      subscription.remove();
-      clearTimeout(fallbackTimer);
-    };
+    // To ensure the user isn't stuck if the payment was cancelled or failed natively,
+    // we provide a manual close button, or rely on them swiping back.
   }, []);
 
   return (
     <View style={styles.container}>
       <ActivityIndicator size="large" color={Colors.neonBlue || "#00F3FF"} />
-      <Text style={styles.text}>正在返回应用...</Text>
+      <Text style={styles.text}>正在确认支付结果...</Text>
+      <Text
+        style={styles.subtext}
+        onPress={() => {
+          if (router.canGoBack()) router.back();
+          else router.replace("/");
+        }}
+      >
+        如长时间未响应,请点击此处返回
+      </Text>
     </View>
   );
 }
@@ -69,6 +56,14 @@ const styles = StyleSheet.create({
   text: {
     marginTop: 20,
     color: Colors.textSecondary || "#aaa",
+    fontSize: 16,
+    fontWeight: "bold",
+  },
+  subtext: {
+    marginTop: 30,
+    color: Colors.neonPink || "#FF007F",
     fontSize: 14,
+    textDecorationLine: "underline",
+    padding: 10,
   },
 });

+ 231 - 82
app/store/components/CheckoutModal.tsx

@@ -1,10 +1,11 @@
-import { Image } from 'expo-image';
-import Alipay from 'expo-native-alipay';
-import { useRouter } from 'expo-router';
-import React, { useEffect, useState } from 'react';
+import { Image } from "expo-image";
+import Alipay from "expo-native-alipay";
+import { useRouter } from "expo-router";
+import React, { useEffect, useState } from "react";
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     ImageBackground,
     Modal,
     Platform,
@@ -13,26 +14,41 @@ import {
     Text,
     TouchableOpacity,
     View,
-} from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
 
-import { Images } from '@/constants/images';
-import { Address, getDefaultAddress } from '@/services/address';
-import { takeApply, takePreview } from '@/services/award';
+import { Images } from "@/constants/images";
+import { Address, getDefaultAddress } from "@/services/address";
+import { takeApply, takePreview } from "@/services/award";
 
 interface GroupedGoods {
   total: number;
-  data: { id: string; cover: string; spuId: string; level: string; name?: string };
+  data: {
+    id: string;
+    cover: string;
+    spuId: string;
+    level: string;
+    name?: string;
+  };
 }
 
 interface CheckoutModalProps {
   visible: boolean;
-  selectedItems: Array<{ id: string; spu: { id: string; name: string; cover: string }; level: string }>;
+  selectedItems: Array<{
+    id: string;
+    spu: { id: string; name: string; cover: string };
+    level: string;
+  }>;
   onClose: () => void;
   onSuccess: () => void;
 }
 
-export default function CheckoutModal({ visible, selectedItems, onClose, onSuccess }: CheckoutModalProps) {
+export default function CheckoutModal({
+  visible,
+  selectedItems,
+  onClose,
+  onSuccess,
+}: CheckoutModalProps) {
   const router = useRouter();
   const insets = useSafeAreaInsets();
   const [loading, setLoading] = useState(false);
@@ -42,8 +58,8 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
   const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
 
   const showAlert = (msg: string) => {
-    if (Platform.OS === 'web') window.alert(msg);
-    else Alert.alert('提示', msg);
+    if (Platform.OS === "web") window.alert(msg);
+    else Alert.alert("提示", msg);
   };
 
   useEffect(() => {
@@ -60,8 +76,8 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
       setAddress(addr);
 
       // 获取提货预览
-      const ids = selectedItems.map(item => item.id);
-      const res = await takePreview(ids, addr?.id || '');
+      const ids = selectedItems.map((item) => item.id);
+      const res = await takePreview(ids, addr?.id || "");
       if (res) {
         setExpressAmount(res.expressAmount || 0);
       }
@@ -87,37 +103,33 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
       });
       setGoodsList(Object.values(goodsMap));
     } catch (e) {
-      console.error('加载提货信息失败:', e);
+      console.error("加载提货信息失败:", e);
     }
     setLoading(false);
   };
 
   const goToAddress = () => {
     onClose();
-    router.push('/address?type=1' as any);
+    router.push("/address?type=1" as any);
   };
 
-  /* 
+  /*
    * Handle Submit with Payment Choice
    */
   const handleSubmit = async () => {
     if (!address) {
-      showAlert('请选择收货地址');
+      showAlert("请选择收货地址");
       return;
     }
-    
+
     if (expressAmount > 0) {
-        Alert.alert(
-            '支付运费',
-            `需支付运费 ¥${expressAmount}`,
-            [
-                { text: '取消', style: 'cancel' },
-                { text: '钱包支付', onPress: () => processTakeApply('WALLET') },
-                { text: '支付宝支付', onPress: () => processTakeApply('ALIPAY_APP') }
-            ]
-        );
+      Alert.alert("支付运费", `需支付运费 ¥${expressAmount}`, [
+        { text: "取消", style: "cancel" },
+        { text: "钱包支付", onPress: () => processTakeApply("WALLET") },
+        { text: "支付宝支付", onPress: () => processTakeApply("ALIPAY_APP") },
+      ]);
     } else {
-        processTakeApply('WALLET');
+      processTakeApply("WALLET");
     }
   };
 
@@ -125,38 +137,86 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
     if (submitting) return;
     setSubmitting(true);
     try {
-      const ids = selectedItems.map(item => item.id);
+      const ids = selectedItems.map((item) => item.id);
       const res: any = await takeApply(ids, address!.id, paymentType);
-      
-      console.log('Take Apply Res:', res, paymentType);
-
-      if (paymentType === 'ALIPAY_APP' && res?.payInfo) {
-           Alipay.setAlipayScheme('alipay2021005175632205');
-           const result = await Alipay.pay(res.payInfo);
-           console.log('Alipay Result:', result);
-           if (result?.resultStatus === '9000') {
-               showAlert('提货成功');
-               onSuccess();
-           } else {
-               showAlert('支付未完成');
-           }
+
+      console.log("Take Apply Res:", res, paymentType);
+
+      if (paymentType === "ALIPAY_APP" && res?.payInfo) {
+        let appStateSub: any = null;
+        let isResolved = false;
+
+        try {
+          Alipay.setAlipayScheme("alipay2021005175632205");
+
+          appStateSub = AppState.addEventListener(
+            "change",
+            async (nextAppState) => {
+              if (nextAppState === "active" && !isResolved) {
+                setTimeout(async () => {
+                  if (!isResolved) {
+                    console.log(
+                      "Alipay SDK did not resolve, assuming success or checking...",
+                    );
+                    // Store doesn't have an easy getApplyResult by tradeNo in this scope,
+                    // but if they returned to the app we can just assume success and
+                    // refresh the page to let the backend state reflect.
+                    isResolved = true;
+                    showAlert("支付完成正在核实");
+                    onSuccess();
+                  }
+                }, 3000);
+              }
+            },
+          );
+
+          const result = await Alipay.pay(res.payInfo);
+          isResolved = true;
+          console.log("Alipay Result:", result);
+
+          if (result?.resultStatus === "9000") {
+            showAlert("提货成功");
+            onSuccess();
+          } else if (result?.resultStatus === "6001") {
+            showAlert("用户取消支付");
+          } else {
+            showAlert("支付未完成");
+          }
+        } catch (e: any) {
+          isResolved = true;
+          console.error("Alipay Error:", e);
+          showAlert("调用支付宝失败");
+        } finally {
+          if (appStateSub) {
+            appStateSub.remove();
+          }
+        }
       } else if (res) {
-         // Wallet payment or free success
-         showAlert('提货成功');
-         onSuccess();
+        // Wallet payment or free success
+        showAlert("提货成功");
+        onSuccess();
       }
     } catch (e) {
-      console.error('提货失败:', e);
+      console.error("提货失败:", e);
       // Usually axios interceptor handles error alerts, but just in case
-      // showAlert('提货失败'); 
+      // showAlert('提货失败');
     }
     setSubmitting(false);
   };
 
   return (
-    <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
+    <Modal
+      visible={visible}
+      transparent
+      animationType="slide"
+      onRequestClose={onClose}
+    >
       <View style={styles.overlay}>
-        <TouchableOpacity style={styles.overlayBg} onPress={onClose} activeOpacity={1} />
+        <TouchableOpacity
+          style={styles.overlayBg}
+          onPress={onClose}
+          activeOpacity={1}
+        />
         <View style={[styles.container, { paddingBottom: insets.bottom + 20 }]}>
           {/* 标题 */}
           <View style={styles.header}>
@@ -177,9 +237,15 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
                 <ScrollView horizontal showsHorizontalScrollIndicator={false}>
                   {goodsList.map((goods, idx) => (
                     <View key={idx} style={styles.goodsItem}>
-                      <Image source={{ uri: goods.data.cover }} style={styles.goodsImg} contentFit="contain" />
+                      <Image
+                        source={{ uri: goods.data.cover }}
+                        style={styles.goodsImg}
+                        contentFit="contain"
+                      />
                       <View style={styles.goodsCount}>
-                        <Text style={styles.goodsCountText}>x{goods.total}</Text>
+                        <Text style={styles.goodsCountText}>
+                          x{goods.total}
+                        </Text>
                       </View>
                     </View>
                   ))}
@@ -195,22 +261,42 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
               )}
 
               {/* 收货地址 */}
-              <TouchableOpacity style={styles.addressSection} onPress={goToAddress}>
+              <TouchableOpacity
+                style={styles.addressSection}
+                onPress={goToAddress}
+              >
                 {!address ? (
                   <Text style={styles.noAddress}>请填写收货地址</Text>
                 ) : (
                   <View style={styles.addressInfo}>
-                    <Text style={styles.addressName}>{address.contactName} {address.contactNo}</Text>
-                    <Text style={styles.addressDetail}>{address.province}{address.city}{address.district}{address.address}</Text>
+                    <Text style={styles.addressName}>
+                      {address.contactName} {address.contactNo}
+                    </Text>
+                    <Text style={styles.addressDetail}>
+                      {address.province}
+                      {address.city}
+                      {address.district}
+                      {address.address}
+                    </Text>
                   </View>
                 )}
                 <Text style={styles.arrowIcon}>›</Text>
               </TouchableOpacity>
 
               {/* 提交按钮 */}
-              <TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={submitting}>
-                <ImageBackground source={{ uri: Images.common.loginBtn }} style={styles.submitBtnBg} resizeMode="contain">
-                  <Text style={styles.submitBtnText}>{submitting ? '提交中...' : '确定发货'}</Text>
+              <TouchableOpacity
+                style={styles.submitBtn}
+                onPress={handleSubmit}
+                disabled={submitting}
+              >
+                <ImageBackground
+                  source={{ uri: Images.common.loginBtn }}
+                  style={styles.submitBtnBg}
+                  resizeMode="contain"
+                >
+                  <Text style={styles.submitBtnText}>
+                    {submitting ? "提交中..." : "确定发货"}
+                  </Text>
                 </ImageBackground>
               </TouchableOpacity>
             </>
@@ -222,29 +308,92 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
 }
 
 const styles = StyleSheet.create({
-  overlay: { flex: 1, justifyContent: 'flex-end' },
-  overlayBg: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)' },
-  container: { backgroundColor: '#fff', borderTopLeftRadius: 15, borderTopRightRadius: 15, paddingHorizontal: 14 },
-  header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 20, position: 'relative' },
-  title: { fontSize: 16, fontWeight: 'bold', color: '#000' },
-  closeBtn: { position: 'absolute', right: 0, top: 15, width: 24, height: 24, backgroundColor: '#ebebeb', borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
-  closeBtnText: { fontSize: 18, color: '#a2a2a2', lineHeight: 20 },
-  loadingBox: { height: 200, justifyContent: 'center', alignItems: 'center' },
+  overlay: { flex: 1, justifyContent: "flex-end" },
+  overlayBg: {
+    position: "absolute",
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    backgroundColor: "rgba(0,0,0,0.5)",
+  },
+  container: {
+    backgroundColor: "#fff",
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 14,
+  },
+  header: {
+    flexDirection: "row",
+    alignItems: "center",
+    justifyContent: "center",
+    paddingVertical: 20,
+    position: "relative",
+  },
+  title: { fontSize: 16, fontWeight: "bold", color: "#000" },
+  closeBtn: {
+    position: "absolute",
+    right: 0,
+    top: 15,
+    width: 24,
+    height: 24,
+    backgroundColor: "#ebebeb",
+    borderRadius: 12,
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  closeBtnText: { fontSize: 18, color: "#a2a2a2", lineHeight: 20 },
+  loadingBox: { height: 200, justifyContent: "center", alignItems: "center" },
   goodsSection: { paddingVertical: 10 },
-  goodsItem: { width: 79, height: 103, backgroundColor: '#fff', borderRadius: 6, marginRight: 8, alignItems: 'center', justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: '#eaeaea' },
+  goodsItem: {
+    width: 79,
+    height: 103,
+    backgroundColor: "#fff",
+    borderRadius: 6,
+    marginRight: 8,
+    alignItems: "center",
+    justifyContent: "center",
+    position: "relative",
+    borderWidth: 1,
+    borderColor: "#eaeaea",
+  },
   goodsImg: { width: 73, height: 85 },
-  goodsCount: { position: 'absolute', top: 0, right: 0, backgroundColor: '#ff6b00', borderRadius: 2, paddingHorizontal: 4, paddingVertical: 2 },
-  goodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
-  feeRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
-  feeLabel: { fontSize: 14, color: '#333' },
-  feeValue: { fontSize: 14, color: '#ff6b00', fontWeight: 'bold' },
-  addressSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderTopWidth: 1, borderTopColor: '#f0f0f0' },
-  noAddress: { flex: 1, fontSize: 16, fontWeight: 'bold', color: '#333' },
+  goodsCount: {
+    position: "absolute",
+    top: 0,
+    right: 0,
+    backgroundColor: "#ff6b00",
+    borderRadius: 2,
+    paddingHorizontal: 4,
+    paddingVertical: 2,
+  },
+  goodsCountText: { color: "#fff", fontSize: 10, fontWeight: "bold" },
+  feeRow: {
+    flexDirection: "row",
+    justifyContent: "space-between",
+    alignItems: "center",
+    paddingVertical: 8,
+  },
+  feeLabel: { fontSize: 14, color: "#333" },
+  feeValue: { fontSize: 14, color: "#ff6b00", fontWeight: "bold" },
+  addressSection: {
+    flexDirection: "row",
+    alignItems: "center",
+    paddingVertical: 10,
+    borderTopWidth: 1,
+    borderTopColor: "#f0f0f0",
+  },
+  noAddress: { flex: 1, fontSize: 16, fontWeight: "bold", color: "#333" },
   addressInfo: { flex: 1 },
-  addressName: { fontSize: 14, color: '#333', fontWeight: 'bold' },
-  addressDetail: { fontSize: 12, color: '#666', marginTop: 4 },
-  arrowIcon: { fontSize: 20, color: '#999', marginLeft: 10 },
-  submitBtn: { alignItems: 'center', marginTop: 15 },
-  submitBtnBg: { width: 260, height: 60, justifyContent: 'center', alignItems: 'center' },
-  submitBtnText: { color: '#000', fontSize: 16, fontWeight: 'bold' },
+  addressName: { fontSize: 14, color: "#333", fontWeight: "bold" },
+  addressDetail: { fontSize: 12, color: "#666", marginTop: 4 },
+  arrowIcon: { fontSize: 20, color: "#999", marginLeft: 10 },
+  submitBtn: { alignItems: "center", marginTop: 15 },
+  submitBtnBg: {
+    width: 260,
+    height: 60,
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  submitBtnText: { color: "#000", fontSize: 16, fontWeight: "bold" },
 });

+ 51 - 5
app/treasure-hunt/components/CheckoutModal.tsx

@@ -9,6 +9,7 @@ import React, {
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     Dimensions,
     Modal,
     ScrollView,
@@ -274,28 +275,73 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       tradeNo: string,
     ) => {
       if (type === "ALIPAY" || type.includes("ALIPAY")) {
+        let appStateSub: any = null;
+        let isResolved = false;
+
         try {
-          // 设置支付宝 URL Scheme(用于支付完成后返回APP)
           Alipay.setAlipayScheme("alipay2021005175632205");
 
-          // 使用 expo-native-alipay 调用支付宝
+          // Watch for app returning to foreground as a fallback
+          appStateSub = AppState.addEventListener(
+            "change",
+            async (nextAppState) => {
+              if (nextAppState === "active" && !isResolved) {
+                // Wait a bit to give the SDK a chance to resolve natively first
+                setTimeout(async () => {
+                  if (!isResolved) {
+                    console.log(
+                      "Alipay SDK did not resolve, checking server manually...",
+                    );
+                    try {
+                      const res = await getApplyResult(tradeNo);
+                      if (
+                        res?.paySuccess ||
+                        (res?.inventoryList && res.inventoryList.length > 0)
+                      ) {
+                        isResolved = true;
+                        handleSuccess(tradeNo);
+                      }
+                    } catch (e) {
+                      console.log("Fallback check failed", e);
+                    }
+                  }
+                }, 3000);
+              }
+            },
+          );
+
           const result = await Alipay.pay(payInfo);
+          isResolved = true;
           console.log("Alipay Result:", result);
 
-          // resultStatus: '9000' 表示支付成功
           const status = result?.resultStatus;
-
           if (status === "9000") {
-            // 支付成功,跳转到抽奖页面
             handleSuccess(tradeNo);
           } else if (status === "6001") {
             Alert.alert("提示", "用户取消支付");
           } else {
+            // Also poll server just in case the status is wrong but it succeeded
+            try {
+              const res = await getApplyResult(tradeNo);
+              if (
+                res?.paySuccess ||
+                (res?.inventoryList && res.inventoryList.length > 0)
+              ) {
+                handleSuccess(tradeNo);
+                return;
+              }
+            } catch (ignore) {}
+
             Alert.alert("支付中断", `状态码: ${status}`);
           }
         } catch (e: any) {
+          isResolved = true;
           console.log("Alipay Error:", e);
           Alert.alert("支付异常", e.message || "调用支付宝失败");
+        } finally {
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
       } else {
         Alert.alert("提示", "微信支付暂未实现");

+ 238 - 166
app/wallet/recharge.tsx

@@ -1,179 +1,251 @@
-import Alipay from 'expo-native-alipay';
-import { Stack, useRouter } from 'expo-router';
-import React, { useState } from 'react';
-import { ActivityIndicator, Alert, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
-import services from '../../services/api';
+import Alipay from "expo-native-alipay";
+import { Stack, useRouter } from "expo-router";
+import React, { useState } from "react";
+import {
+    ActivityIndicator,
+    Alert,
+    AppState,
+    StyleSheet,
+    Text,
+    TextInput,
+    TouchableOpacity,
+    View,
+} from "react-native";
+import services from "../../services/api";
 
 const MAX_AMOUNT = 100000;
 
 export default function RechargeScreen() {
-    const router = useRouter();
-    const [money, setMoney] = useState('');
-    const [loading, setLoading] = useState(false);
-
-    const validateInput = (text: string) => {
-        let value = text.replace(/[^\d.]/g, '')
-            .replace(/^\./g, '')
-            .replace(/\.{2,}/g, '.');
-        
-        value = value.replace(/^(-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
-
-        if (parseFloat(value) > MAX_AMOUNT) {
-            Alert.alert('提示', `最大充值金额为${MAX_AMOUNT}元`);
-            value = MAX_AMOUNT.toString();
-        }
-        setMoney(value);
-    };
-
-    const handleRecharge = async () => {
-        const amount = parseFloat(money);
-        if (!amount || amount <= 0) {
-            Alert.alert('提示', '请输入有效的充值金额');
-            return;
-        }
+  const router = useRouter();
+  const [money, setMoney] = useState("");
+  const [loading, setLoading] = useState(false);
+
+  const validateInput = (text: string) => {
+    let value = text
+      .replace(/[^\d.]/g, "")
+      .replace(/^\./g, "")
+      .replace(/\.{2,}/g, ".");
+
+    value = value.replace(/^(-)*(\d+)\.(\d\d).*$/, "$1$2.$3");
+
+    if (parseFloat(value) > MAX_AMOUNT) {
+      Alert.alert("提示", `最大充值金额为${MAX_AMOUNT}元`);
+      value = MAX_AMOUNT.toString();
+    }
+    setMoney(value);
+  };
+
+  const handleRecharge = async () => {
+    const amount = parseFloat(money);
+    if (!amount || amount <= 0) {
+      Alert.alert("提示", "请输入有效的充值金额");
+      return;
+    }
+
+    setLoading(true);
+    setLoading(true);
+    try {
+      // Use ALIPAY_APP for native payment with correct API
+      const res: any = await services.wallet.rechargeSubmit(
+        amount,
+        "ALIPAY_APP",
+        "CASH",
+      );
+
+      console.log("Recharge API Res:", res);
+
+      // Handle response which might be direct string or object with payInfo
+      const payInfo =
+        typeof res?.data === "string"
+          ? res?.data
+          : res?.data?.payInfo || res?.data?.orderInfo || res?.data?.tradeNo;
+
+      if (
+        payInfo &&
+        typeof payInfo === "string" &&
+        (payInfo.startsWith("alipay_root_cert_sn") ||
+          payInfo.includes("alipay_sdk"))
+      ) {
+        let appStateSub: any = null;
+        let isResolved = false;
 
-        setLoading(true);
-        setLoading(true);
         try {
-            // Use ALIPAY_APP for native payment with correct API
-            const res: any = await services.wallet.rechargeSubmit(amount, 'ALIPAY_APP', 'CASH');
-            
-            console.log('Recharge API Res:', res);
-
-            // Handle response which might be direct string or object with payInfo
-            const payInfo = typeof res?.data === 'string' ? res?.data : (res?.data?.payInfo || res?.data?.orderInfo || res?.data?.tradeNo);
-
-            if (payInfo && typeof payInfo === 'string' && (payInfo.startsWith('alipay_root_cert_sn') || payInfo.includes('alipay_sdk'))) {
-                 Alipay.setAlipayScheme('alipay2021005175632205');
-                 const result = await Alipay.pay(payInfo);
-                 console.log('Alipay Result:', result);
-                 
-                 if (result?.resultStatus === '9000') {
-                     Alert.alert('提示', '充值成功');
-                     router.replace('/wallet/recharge_record');
-                 } else {
-                     Alert.alert('提示', '支付未完成');
-                 }
-            } else if (res?.data?.tradeNo && !payInfo) {
-                 Alert.alert('提示', '获取支付信息失败(仅获取到订单号)');
-            } else {
-                 Alert.alert('失败', '生成充值订单失败 ' + (res?.msg || ''));
-            }
-
-        } catch (error) {
-            console.error('Recharge failed', error);
-            Alert.alert('错误', '充值失败,请重试');
+          Alipay.setAlipayScheme("alipay2021005175632205");
+
+          appStateSub = AppState.addEventListener(
+            "change",
+            async (nextAppState) => {
+              if (nextAppState === "active" && !isResolved) {
+                setTimeout(async () => {
+                  if (!isResolved) {
+                    console.log(
+                      "Alipay SDK did not resolve natively, assuming success to refresh...",
+                    );
+                    isResolved = true;
+                    Alert.alert("提示", "充值已发起,请稍后查看余额");
+                    router.replace("/wallet/recharge_record");
+                  }
+                }, 3000);
+              }
+            },
+          );
+
+          const result = await Alipay.pay(payInfo);
+          isResolved = true;
+          console.log("Alipay Result:", result);
+
+          if (result?.resultStatus === "9000") {
+            Alert.alert("提示", "充值成功");
+            router.replace("/wallet/recharge_record");
+          } else if (result?.resultStatus === "6001") {
+            Alert.alert("提示", "用户取消支付");
+          } else {
+            Alert.alert("提示", "支付未完成");
+          }
+        } catch (e: any) {
+          isResolved = true;
+          Alert.alert("提示", "调用支付宝失败: " + (e.message || ""));
         } finally {
-            setLoading(false);
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
-    };
-
-    return (
-        <View style={styles.container}>
-            <Stack.Screen options={{ title: '充值', headerStyle: { backgroundColor: '#fff' }, headerShadowVisible: false }} />
-            
-            <View style={styles.content}>
-                <View style={styles.inputWrapper}>
-                    <Text style={styles.label}>充值金额:</Text>
-                    <TextInput
-                        style={styles.input}
-                        value={money}
-                        onChangeText={validateInput}
-                        placeholder="请输入金额"
-                        keyboardType="decimal-pad"
-                    />
-                </View>
-
-                <Text style={styles.tip}>充值金额可以用于购买手办,奖池</Text>
-                <Text style={styles.tipSub}>
-                    充值金额不可提现,<Text style={styles.highlight}>线下充值额外返10%</Text>
-                </Text>
-
-                <TouchableOpacity 
-                    style={[styles.btn, loading && styles.disabledBtn]} 
-                    onPress={handleRecharge}
-                    disabled={loading}
-                >
-                     {loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.btnText}>充值</Text>}
-                </TouchableOpacity>
-
-                <TouchableOpacity style={styles.recordLink} onPress={() => router.push('/wallet/recharge_record')}>
-                    <Text style={styles.recordLinkText}>充值记录 {'>'}{'>'}</Text>
-                </TouchableOpacity>
-            </View>
+      } else if (res?.data?.tradeNo && !payInfo) {
+        Alert.alert("提示", "获取支付信息失败(仅获取到订单号)");
+      } else {
+        Alert.alert("失败", "生成充值订单失败 " + (res?.msg || ""));
+      }
+    } catch (error) {
+      console.error("Recharge failed", error);
+      Alert.alert("错误", "充值失败,请重试");
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <View style={styles.container}>
+      <Stack.Screen
+        options={{
+          title: "充值",
+          headerStyle: { backgroundColor: "#fff" },
+          headerShadowVisible: false,
+        }}
+      />
+
+      <View style={styles.content}>
+        <View style={styles.inputWrapper}>
+          <Text style={styles.label}>充值金额:</Text>
+          <TextInput
+            style={styles.input}
+            value={money}
+            onChangeText={validateInput}
+            placeholder="请输入金额"
+            keyboardType="decimal-pad"
+          />
         </View>
-    );
+
+        <Text style={styles.tip}>充值金额可以用于购买手办,奖池</Text>
+        <Text style={styles.tipSub}>
+          充值金额不可提现,
+          <Text style={styles.highlight}>线下充值额外返10%</Text>
+        </Text>
+
+        <TouchableOpacity
+          style={[styles.btn, loading && styles.disabledBtn]}
+          onPress={handleRecharge}
+          disabled={loading}
+        >
+          {loading ? (
+            <ActivityIndicator color="#fff" />
+          ) : (
+            <Text style={styles.btnText}>充值</Text>
+          )}
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={styles.recordLink}
+          onPress={() => router.push("/wallet/recharge_record")}
+        >
+          <Text style={styles.recordLinkText}>
+            充值记录 {">"}
+            {">"}
+          </Text>
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
 }
 
 const styles = StyleSheet.create({
-    container: {
-        flex: 1,
-        backgroundColor: '#fff',
-    },
-    content: {
-        paddingHorizontal: 24,
-        paddingTop: 32,
-    },
-    inputWrapper: {
-        flexDirection: 'row',
-        alignItems: 'center',
-        borderWidth: 1,
-        borderColor: '#ddd',
-        borderRadius: 8,
-        paddingHorizontal: 16,
-        height: 52, // 104rpx
-        marginTop: 15,
-    },
-    label: {
-        fontSize: 16, // font5
-        color: '#333',
-        marginRight: 10,
-    },
-    input: {
-        flex: 1,
-        fontSize: 16, // font5
-        height: '100%',
-    },
-    tip: {
-        textAlign: 'center',
-        color: '#666', // color-2
-        fontSize: 14,
-        marginTop: 36,
-    },
-    tipSub: {
-        textAlign: 'center',
-        color: '#666', // color-2
-        fontSize: 14,
-        marginTop: 5,
-        marginBottom: 20,
-    },
-    highlight: {
-        color: '#07C160',
-        textDecorationLine: 'underline',
-    },
-    btn: {
-        backgroundColor: '#0081ff', // bg-blue
-        height: 44,
-        borderRadius: 4,
-        justifyContent: 'center',
-        alignItems: 'center',
-        width: '70%',
-        alignSelf: 'center',
-    },
-    disabledBtn: {
-        opacity: 0.7,
-    },
-    btnText: {
-        color: '#fff',
-        fontSize: 14,
-        fontWeight: 'bold',
-    },
-    recordLink: {
-        marginTop: 20,
-        alignItems: 'center',
-    },
-    recordLinkText: {
-        color: '#8b3dff', // color-theme
-        fontSize: 14,
-    },
+  container: {
+    flex: 1,
+    backgroundColor: "#fff",
+  },
+  content: {
+    paddingHorizontal: 24,
+    paddingTop: 32,
+  },
+  inputWrapper: {
+    flexDirection: "row",
+    alignItems: "center",
+    borderWidth: 1,
+    borderColor: "#ddd",
+    borderRadius: 8,
+    paddingHorizontal: 16,
+    height: 52, // 104rpx
+    marginTop: 15,
+  },
+  label: {
+    fontSize: 16, // font5
+    color: "#333",
+    marginRight: 10,
+  },
+  input: {
+    flex: 1,
+    fontSize: 16, // font5
+    height: "100%",
+  },
+  tip: {
+    textAlign: "center",
+    color: "#666", // color-2
+    fontSize: 14,
+    marginTop: 36,
+  },
+  tipSub: {
+    textAlign: "center",
+    color: "#666", // color-2
+    fontSize: 14,
+    marginTop: 5,
+    marginBottom: 20,
+  },
+  highlight: {
+    color: "#07C160",
+    textDecorationLine: "underline",
+  },
+  btn: {
+    backgroundColor: "#0081ff", // bg-blue
+    height: 44,
+    borderRadius: 4,
+    justifyContent: "center",
+    alignItems: "center",
+    width: "70%",
+    alignSelf: "center",
+  },
+  disabledBtn: {
+    opacity: 0.7,
+  },
+  btnText: {
+    color: "#fff",
+    fontSize: 14,
+    fontWeight: "bold",
+  },
+  recordLink: {
+    marginTop: 20,
+    alignItems: "center",
+  },
+  recordLinkText: {
+    color: "#8b3dff", // color-theme
+    fontSize: 14,
+  },
 });