4 Commits 5896c88d5e ... 05686de1a8

Auteur SHA1 Message Date
  zbb 05686de1a8 fix: 修复支付宝回调延迟与多层页面Bug il y a 1 mois
  zbb 87750b54e3 fix(alipay): add navigation locks to CheckoutModal and happy-spin to prevent overlapping multi-callbacks causing stacked screens il y a 1 mois
  zbb b154d69188 fix(alipay): remove safepay UI and convert AppState fallback to immediate polling for instant animation il y a 1 mois
  zbb 42d5ba64f4 fix(alipay): add AppState polling fallback to fix TestFlight callback interruption il y a 1 mois

+ 1 - 1
app.json

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

+ 120 - 5
app/award-detail/components/CheckoutModal.tsx

@@ -9,6 +9,7 @@ import React, {
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     Dimensions,
     Modal,
     ScrollView,
@@ -259,9 +260,17 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       }
     };
 
+    const [verifyLoading, setVerifyLoading] = useState(false);
+    const isNavigatingRef = useRef(false);
+
     const handleSuccess = (tradeNo: string) => {
+      setVerifyLoading(false);
       setVisible(false);
-      router.push({
+
+      if (isNavigatingRef.current) return;
+      isNavigatingRef.current = true;
+
+      router.replace({
         pathname: "/lottery" as any,
         params: { tradeNo, num, poolId },
       });
@@ -274,28 +283,101 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       tradeNo: string,
     ) => {
       if (type === "ALIPAY" || type.includes("ALIPAY")) {
+        let appStateSub: any = null;
+        let isResolved = false;
+        let pollingStarted = 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 && !pollingStarted) {
+                pollingStarted = true;
+                const loadingTimer = setTimeout(() => {
+                  if (!isResolved) setVerifyLoading(true);
+                }, 800);
+                let attempts = 0;
+                const pollInterval = setInterval(async () => {
+                  if (isResolved) {
+                    clearInterval(pollInterval);
+                    return;
+                  }
+                  attempts++;
+                  try {
+                    const res = await getApplyResult(tradeNo);
+                    // 关键修复:await 返回后再次检查 isResolved,防止竞态
+                    if (isResolved) {
+                      clearInterval(pollInterval);
+                      return;
+                    }
+                    if (
+                      res?.paySuccess ||
+                      (res?.inventoryList && res.inventoryList.length > 0)
+                    ) {
+                      isResolved = true;
+                      clearInterval(pollInterval);
+                      clearTimeout(loadingTimer);
+                      setVerifyLoading(false);
+                      handleSuccess(tradeNo);
+                    }
+                  } catch (e) {
+                    console.log("Fallback poll failed", e);
+                  }
+                  if (attempts >= 15) {
+                    clearInterval(pollInterval);
+                    clearTimeout(loadingTimer);
+                    setVerifyLoading(false);
+                  }
+                }, 300);
+              }
+            },
+          );
+
           const result = await Alipay.pay(payInfo);
+          if (isResolved) return; // if fallback already worked
+          isResolved = true;
+          setVerifyLoading(false);
           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 {
+              setVerifyLoading(true);
+              const res = await getApplyResult(tradeNo);
+              if (
+                res?.paySuccess ||
+                (res?.inventoryList && res.inventoryList.length > 0)
+              ) {
+                handleSuccess(tradeNo);
+                return;
+              }
+            } catch (ignore) {
+            } finally {
+              setVerifyLoading(false);
+            }
+
             Alert.alert("支付中断", `状态码: ${status}`);
           }
         } catch (e: any) {
+          if (isResolved) return;
+          isResolved = true;
+          setVerifyLoading(false);
           console.log("Alipay Error:", e);
           Alert.alert("支付异常", e.message || "调用支付宝失败");
+        } finally {
+          setVerifyLoading(false);
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
       } else {
         Alert.alert("提示", "微信支付暂未实现");
@@ -649,12 +731,45 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
             </View>
           </View>
         </Modal>
+
+        {/* 支付结果轮询确认加载中 */}
+        <Modal
+          visible={verifyLoading}
+          transparent
+          animationType="fade"
+          onRequestClose={() => {}}
+        >
+          <View style={styles.verifyLoadingOverlay}>
+            <View style={styles.verifyLoadingBox}>
+              <ActivityIndicator size="large" color="#ff9600" />
+              <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
+            </View>
+          </View>
+        </Modal>
       </>
     );
   },
 );
 
 const styles = StyleSheet.create({
+  verifyLoadingOverlay: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: "rgba(0,0,0,0.4)",
+    justifyContent: "center",
+    alignItems: "center",
+    zIndex: 9999,
+  },
+  verifyLoadingBox: {
+    backgroundColor: "rgba(0,0,0,0.8)",
+    padding: 20,
+    borderRadius: 12,
+    alignItems: "center",
+  },
+  verifyLoadingText: {
+    color: "#fff",
+    marginTop: 10,
+    fontSize: 14,
+  },
   overlay: {
     flex: 1,
     backgroundColor: "rgba(0,0,0,0.5)",

+ 262 - 82
app/cloud-warehouse/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);
@@ -41,9 +57,11 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
   const [expressAmount, setExpressAmount] = useState(0);
   const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
 
+  const [verifyLoading, setVerifyLoading] = useState(false);
+
   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 +78,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 +105,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 +139,82 @@ 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) {
+                    isResolved = true;
+                    setVerifyLoading(false);
+                    showAlert("支付完成正在核实");
+                    onSuccess();
+                  }
+                }, 500);
+              }
+            },
+          );
+
+          const result = await Alipay.pay(res.payInfo);
+          if (isResolved) return;
+          isResolved = true;
+          setVerifyLoading(false);
+          console.log("Alipay Result:", result);
+          if (result?.resultStatus === "9000") {
+            showAlert("提货成功");
+            onSuccess();
+          } else {
+            showAlert("支付未完成");
+          }
+        } catch (e) {
+          isResolved = true;
+          setVerifyLoading(false);
+          console.error("Alipay Error:", e);
+          showAlert("调用支付宝失败");
+        } finally {
+          setVerifyLoading(false);
+          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 +235,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,56 +259,172 @@ 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>
             </>
           )}
         </View>
       </View>
+
+      {/* 支付结果轮询确认加载中 */}
+      <Modal
+        visible={verifyLoading}
+        transparent
+        animationType="fade"
+        onRequestClose={() => {}}
+      >
+        <View style={styles.verifyLoadingOverlay}>
+          <View style={styles.verifyLoadingBox}>
+            <ActivityIndicator size="large" color="#ff9600" />
+            <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
+          </View>
+        </View>
+      </Modal>
     </Modal>
   );
 }
 
 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' },
+  verifyLoadingOverlay: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: "rgba(0,0,0,0.4)",
+    justifyContent: "center",
+    alignItems: "center",
+    zIndex: 9999,
+  },
+  verifyLoadingBox: {
+    backgroundColor: "rgba(0,0,0,0.8)",
+    padding: 20,
+    borderRadius: 12,
+    alignItems: "center",
+  },
+  verifyLoadingText: {
+    color: "#fff",
+    marginTop: 10,
+    fontSize: 14,
+  },
+  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" },
 });

+ 4 - 0
app/happy-spin/index.tsx

@@ -41,6 +41,7 @@ export default function LotteryScreen() {
   const [loading, setLoading] = useState(true);
   const [sound, setSound] = useState<Audio.Sound>();
   const [animationFinished, setAnimationFinished] = useState(false);
+  const isFinishedRef = useRef(false);
 
   // Video state
   const [videoVisible, setVideoVisible] = useState(false);
@@ -154,6 +155,9 @@ export default function LotteryScreen() {
       timerRef.current = null;
     }
 
+    if (isFinishedRef.current) return;
+    isFinishedRef.current = true;
+
     if (isGrid) {
       // Stop sound and mark finished, stay on page
       sound?.stopAsync();

+ 45 - 9
app/purse/recharge.tsx

@@ -4,6 +4,7 @@ import React, { useState } from "react";
 import {
     ActivityIndicator,
     Alert,
+    AppState,
     StyleSheet,
     Text,
     TextInput,
@@ -65,15 +66,50 @@ 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");
+                  }
+                }, 500); // 0.5秒后提示充值已发起
+              }
+            },
+          );
+
+          const result = await Alipay.pay(payInfo);
+          if (isResolved) return;
+          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("提示", "获取支付信息失败(仅获取到订单号)");

+ 3 - 68
app/safepay.tsx

@@ -1,74 +1,9 @@
-import { Colors } from "@/constants/Colors";
-import { useLocalSearchParams, useRouter } from "expo-router";
-import { useEffect } from "react";
-import {
-    ActivityIndicator,
-    AppState,
-    StyleSheet,
-    Text,
-    View,
-} from "react-native";
-
 /**
  * Alipay Callback Handler
  * Handles the "asios://safepay" deep link from Alipay.
- * Automatically goes back to invoke the native SDK's callback handling.
+ * 纯占位页面,仅防止 Expo Router 报 Unmatched Route 错误。
+ * 不做任何导航操作,支付结果由 CheckoutModal 中的 AppState 轮询获取。
  */
 export default function AlipayResult() {
-  const router = useRouter();
-  const params = useLocalSearchParams();
-
-  useEffect(() => {
-    console.log("Alipay callback params:", params);
-
-    let hasNavigated = false;
-    let fallbackTimer: ReturnType<typeof setTimeout>;
-
-    const navigateBack = () => {
-      if (hasNavigated) return;
-      hasNavigated = true;
-      if (router.canGoBack()) {
-        router.back();
-      } else {
-        router.replace("/");
-      }
-    };
-
-    // 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);
-    };
-  }, []);
-
-  return (
-    <View style={styles.container}>
-      <ActivityIndicator size="large" color={Colors.neonBlue || "#00F3FF"} />
-      <Text style={styles.text}>正在返回应用...</Text>
-    </View>
-  );
+  return null;
 }
-
-const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    justifyContent: "center",
-    alignItems: "center",
-    backgroundColor: Colors.darkBg || "#121212",
-  },
-  text: {
-    marginTop: 20,
-    color: Colors.textSecondary || "#aaa",
-    fontSize: 14,
-  },
-});

+ 270 - 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,37 +14,53 @@ 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);
+  const [verifyLoading, setVerifyLoading] = useState(false);
   const [submitting, setSubmitting] = useState(false);
   const [address, setAddress] = useState<Address | null>(null);
   const [expressAmount, setExpressAmount] = useState(0);
   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 +77,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 +104,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 +138,91 @@ 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;
+                    setVerifyLoading(false);
+                    showAlert("支付完成正在核实");
+                    onSuccess();
+                  }
+                }, 500); // 瞬间回退没有拿到原生支付结果时,0.5秒后默认成功并刷新列表验证
+              }
+            },
+          );
+
+          const result = await Alipay.pay(res.payInfo);
+          if (isResolved) return;
+          isResolved = true;
+          setVerifyLoading(false);
+          console.log("Alipay Result:", result);
+
+          if (result?.resultStatus === "9000") {
+            showAlert("提货成功");
+            onSuccess();
+          } else if (result?.resultStatus === "6001") {
+            showAlert("用户取消支付");
+          } else {
+            showAlert("支付未完成");
+          }
+        } catch (e: any) {
+          isResolved = true;
+          setVerifyLoading(false);
+          console.error("Alipay Error:", e);
+          showAlert("调用支付宝失败");
+        } finally {
+          setVerifyLoading(false);
+          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 +243,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,56 +267,172 @@ 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>
             </>
           )}
         </View>
       </View>
+
+      {/* 支付结果轮询确认加载中 */}
+      <Modal
+        visible={verifyLoading}
+        transparent
+        animationType="fade"
+        onRequestClose={() => {}}
+      >
+        <View style={styles.verifyLoadingOverlay}>
+          <View style={styles.verifyLoadingBox}>
+            <ActivityIndicator size="large" color="#ff9600" />
+            <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
+          </View>
+        </View>
+      </Modal>
     </Modal>
   );
 }
 
 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' },
+  verifyLoadingOverlay: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: "rgba(0,0,0,0.4)",
+    justifyContent: "center",
+    alignItems: "center",
+    zIndex: 9999,
+  },
+  verifyLoadingBox: {
+    backgroundColor: "rgba(0,0,0,0.8)",
+    padding: 20,
+    borderRadius: 12,
+    alignItems: "center",
+  },
+  verifyLoadingText: {
+    color: "#fff",
+    marginTop: 10,
+    fontSize: 14,
+  },
+  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" },
 });

+ 136 - 22
app/treasure-hunt/components/CheckoutModal.tsx

@@ -1,28 +1,29 @@
 import { Image } from "expo-image";
 import { useRouter } from "expo-router";
 import React, {
-    forwardRef,
-    useImperativeHandle,
-    useRef,
-    useState,
+  forwardRef,
+  useImperativeHandle,
+  useRef,
+  useState,
 } from "react";
 import {
-    ActivityIndicator,
-    Alert,
-    Dimensions,
-    Modal,
-    ScrollView,
-    StyleSheet,
-    Text,
-    TouchableOpacity,
-    View,
+  ActivityIndicator,
+  Alert,
+  AppState,
+  Dimensions,
+  Modal,
+  ScrollView,
+  StyleSheet,
+  Text,
+  TouchableOpacity,
+  View,
 } from "react-native";
 
 import { applyOrder, getApplyResult, previewOrder } from "@/services/award";
 import Alipay from "expo-native-alipay";
 import {
-    LotteryResultModal,
-    LotteryResultModalRef,
+  LotteryResultModal,
+  LotteryResultModalRef,
 } from "./LotteryResultModal";
 
 const { width: SCREEN_WIDTH } = Dimensions.get("window");
@@ -259,10 +260,18 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       }
     };
 
+    const [verifyLoading, setVerifyLoading] = useState(false);
+    const isNavigatingRef = useRef(false);
+
     const handleSuccess = (tradeNo: string) => {
+      setVerifyLoading(false);
       setVisible(false);
-      router.push({
-        pathname: "/happy-spin" as any,
+
+      if (isNavigatingRef.current) return;
+      isNavigatingRef.current = true;
+
+      router.replace({
+        pathname: "/treasure-hunt/happy-spin" as any,
         params: { tradeNo, num, poolId },
       });
       onSuccess({ tradeNo, num });
@@ -274,28 +283,100 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
       tradeNo: string,
     ) => {
       if (type === "ALIPAY" || type.includes("ALIPAY")) {
+        let appStateSub: any = null;
+        let isResolved = false;
+        let pollingStarted = 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 && !pollingStarted) {
+                pollingStarted = true;
+                const loadingTimer = setTimeout(() => {
+                  if (!isResolved) setVerifyLoading(true);
+                }, 800);
+                let attempts = 0;
+                const pollInterval = setInterval(async () => {
+                  if (isResolved) {
+                    clearInterval(pollInterval);
+                    return;
+                  }
+                  attempts++;
+                  try {
+                    const res = await getApplyResult(tradeNo);
+                    // 关键修复:await 返回后再次检查 isResolved,防止竞态
+                    if (isResolved) {
+                      clearInterval(pollInterval);
+                      return;
+                    }
+                    if (
+                      res?.paySuccess ||
+                      (res?.inventoryList && res.inventoryList.length > 0)
+                    ) {
+                      isResolved = true;
+                      clearInterval(pollInterval);
+                      clearTimeout(loadingTimer);
+                      setVerifyLoading(false);
+                      handleSuccess(tradeNo);
+                    }
+                  } catch (e) {
+                    console.log("Fallback poll failed", e);
+                  }
+                  if (attempts >= 15) {
+                    clearInterval(pollInterval);
+                    clearTimeout(loadingTimer);
+                    setVerifyLoading(false);
+                  }
+                }, 300);
+              }
+            },
+          );
+
           const result = await Alipay.pay(payInfo);
+          if (isResolved) return; // if fallback already worked
+          isResolved = true;
+          setVerifyLoading(false);
           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 {
+              setVerifyLoading(true);
+              const res = await getApplyResult(tradeNo);
+              if (
+                res?.paySuccess ||
+                (res?.inventoryList && res.inventoryList.length > 0)
+              ) {
+                handleSuccess(tradeNo);
+                return;
+              }
+            } catch (ignore) {
+            } finally {
+              setVerifyLoading(false);
+            }
+
             Alert.alert("支付中断", `状态码: ${status}`);
           }
         } catch (e: any) {
+          if (isResolved) return;
+          isResolved = true;
+          setVerifyLoading(false);
           console.log("Alipay Error:", e);
           Alert.alert("支付异常", e.message || "调用支付宝失败");
+        } finally {
+          setVerifyLoading(false);
+          if (appStateSub) {
+            appStateSub.remove();
+          }
         }
       } else {
         Alert.alert("提示", "微信支付暂未实现");
@@ -649,12 +730,45 @@ export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
             </View>
           </View>
         </Modal>
+
+        {/* 支付结果轮询确认加载中 */}
+        <Modal
+          visible={verifyLoading}
+          transparent
+          animationType="fade"
+          onRequestClose={() => {}}
+        >
+          <View style={styles.verifyLoadingOverlay}>
+            <View style={styles.verifyLoadingBox}>
+              <ActivityIndicator size="large" color="#ff9600" />
+              <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
+            </View>
+          </View>
+        </Modal>
       </>
     );
   },
 );
 
 const styles = StyleSheet.create({
+  verifyLoadingOverlay: {
+    ...StyleSheet.absoluteFillObject,
+    backgroundColor: "rgba(0,0,0,0.4)",
+    justifyContent: "center",
+    alignItems: "center",
+    zIndex: 9999,
+  },
+  verifyLoadingBox: {
+    backgroundColor: "rgba(0,0,0,0.8)",
+    padding: 20,
+    borderRadius: 12,
+    alignItems: "center",
+  },
+  verifyLoadingText: {
+    color: "#fff",
+    marginTop: 10,
+    fontSize: 14,
+  },
   overlay: {
     flex: 1,
     backgroundColor: "rgba(0,0,0,0.5)",

+ 239 - 166
app/wallet/recharge.tsx

@@ -1,179 +1,252 @@
-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");
+                  }
+                }, 500); // 0.5秒后提示充值已发起
+              }
+            },
+          );
+
+          const result = await Alipay.pay(payInfo);
+          if (isResolved) return;
+          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,
+  },
 });

+ 176 - 0
package-lock.json

@@ -7,6 +7,7 @@
     "": {
       "name": "asios",
       "version": "1.0.0",
+      "hasInstallScript": true,
       "dependencies": {
         "@expo/vector-icons": "^15.0.3",
         "@react-native-async-storage/async-storage": "^2.2.0",
@@ -47,6 +48,7 @@
         "@types/react": "~19.1.0",
         "eslint": "^9.25.0",
         "eslint-config-expo": "~10.0.0",
+        "patch-package": "^8.0.1",
         "typescript": "~5.9.2"
       }
     },
@@ -5176,6 +5178,13 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
     "node_modules/abort-controller": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -8347,6 +8356,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/find-yarn-workspace-root": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+      "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "micromatch": "^4.0.2"
+      }
+    },
     "node_modules/flat-cache": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -8414,6 +8433,21 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/fs-extra": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+      "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -9790,6 +9824,26 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/json-stable-stringify": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
+      "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "isarray": "^2.0.5",
+        "jsonify": "^0.0.1",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
@@ -9809,6 +9863,29 @@
         "node": ">=6"
       }
     },
+    "node_modules/jsonfile": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
+      "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+      "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+      "dev": true,
+      "license": "Public Domain",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/jsx-ast-utils": {
       "version": "3.3.5",
       "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -9835,6 +9912,16 @@
         "json-buffer": "3.0.1"
       }
     },
+    "node_modules/klaw-sync": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+      "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.11"
+      }
+    },
     "node_modules/kleur": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
@@ -11618,6 +11705,75 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/patch-package": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
+      "integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@yarnpkg/lockfile": "^1.1.0",
+        "chalk": "^4.1.2",
+        "ci-info": "^3.7.0",
+        "cross-spawn": "^7.0.3",
+        "find-yarn-workspace-root": "^2.0.0",
+        "fs-extra": "^10.0.0",
+        "json-stable-stringify": "^1.0.2",
+        "klaw-sync": "^6.0.0",
+        "minimist": "^1.2.6",
+        "open": "^7.4.2",
+        "semver": "^7.5.3",
+        "slash": "^2.0.0",
+        "tmp": "^0.2.4",
+        "yaml": "^2.2.2"
+      },
+      "bin": {
+        "patch-package": "index.js"
+      },
+      "engines": {
+        "node": ">=14",
+        "npm": ">5"
+      }
+    },
+    "node_modules/patch-package/node_modules/ci-info": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/patch-package/node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/patch-package/node_modules/slash": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+      "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -13695,6 +13851,16 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/tmp": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
     "node_modules/tmpl": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -14014,6 +14180,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
     "node_modules/unpipe": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

+ 3 - 1
package.json

@@ -8,7 +8,8 @@
     "android": "expo run:android",
     "ios": "expo run:ios",
     "web": "expo start --web",
-    "lint": "expo lint"
+    "lint": "expo lint",
+    "postinstall": "patch-package"
   },
   "dependencies": {
     "@expo/vector-icons": "^15.0.3",
@@ -50,6 +51,7 @@
     "@types/react": "~19.1.0",
     "eslint": "^9.25.0",
     "eslint-config-expo": "~10.0.0",
+    "patch-package": "^8.0.1",
     "typescript": "~5.9.2"
   },
   "private": true

+ 180 - 0
patches/expo-native-alipay+0.1.1.patch

@@ -0,0 +1,180 @@
+diff --git a/node_modules/expo-native-alipay/ios/ExpoNativeAlipayDelegate.swift b/node_modules/expo-native-alipay/ios/ExpoNativeAlipayDelegate.swift
+index d5c3506..331addd 100644
+--- a/node_modules/expo-native-alipay/ios/ExpoNativeAlipayDelegate.swift
++++ b/node_modules/expo-native-alipay/ios/ExpoNativeAlipayDelegate.swift
+@@ -9,34 +9,17 @@ import ExpoModulesCore
+ import AlipaySDK
+ 
+ public class ExpoNativeAlipayDelegate: ExpoAppDelegateSubscriber {
+-    // 适配 iOS 9 及以上
+-    public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:], appContext: AppContext?) -> Bool {
++    // 适配标准 UIApplicationDelegate (3 参数签名,匹配 ExpoAppDelegateSubscriberManager 的调用)
++    public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+         if url.host == "safepay" {
+             // 支付跳转支付宝钱包进行支付,处理支付结果
+             AlipaySDK.defaultService()?.processOrder(withPaymentResult: url, standbyCallback: { resultDic in
+-                self.sendAlipayResult(resultDic, appContext: appContext)
++                self.sendAlipayResult(resultDic)
+             })
+ 
+             // 授权跳转支付宝钱包进行支付,处理支付结果
+             AlipaySDK.defaultService()?.processAuth_V2Result(url, standbyCallback: { resultDic in
+-                self.sendAlipayResult(resultDic, appContext: appContext)
+-            })
+-            return true
+-        }
+-        return false
+-    }
+-
+-    // 兼容 iOS 8 及以下(如需支持)
+-    public func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any, appContext: AppContext?) -> Bool {
+-        if url.host == "safepay" {
+-            // 支付跳转支付宝钱包进行支付,处理支付结果
+-            AlipaySDK.defaultService()?.processOrder(withPaymentResult: url, standbyCallback: { resultDic in
+-                self.sendAlipayResult(resultDic, appContext: appContext)
+-            })
+-            
+-            // 授权跳转支付宝钱包进行支付,处理支付结果
+-            AlipaySDK.defaultService()?.processAuth_V2Result(url, standbyCallback: { resultDic in
+-                self.sendAlipayResult(resultDic, appContext: appContext)
++                self.sendAlipayResult(resultDic)
+             })
+             return true
+         }
+@@ -44,36 +27,26 @@ public class ExpoNativeAlipayDelegate: ExpoAppDelegateSubscriber {
+     }
+ 
+     // 处理 Universal Link 回调
+-    public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void, appContext: AppContext?) -> Bool {
++    public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
+         if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
+             AlipaySDK.defaultService()?.handleOpenUniversalLink(userActivity, standbyCallback: { resultDic in
+-                self.sendAlipayResult(resultDic, appContext: appContext)
++                self.sendAlipayResult(resultDic)
+             })
+         }
+         return true
+     }
+ 
+-    private func sendAlipayResult(_ resultDic: [AnyHashable: Any]?, appContext: AppContext?) {
+-        var module: ExpoNativeAlipayModule? = nil
+-
+-        if let appContext = appContext {
+-            // 新版优先
+-            if let moduleRegistry = appContext.value(forKey: "moduleRegistry") as? NSObject,
+-               let m = moduleRegistry.perform(NSSelectorFromString("getModuleImplementingProtocol:"), with: ExpoNativeAlipayModule.self)?.takeUnretainedValue() as? ExpoNativeAlipayModule {
+-                module = m
+-            } else if let m = appContext.perform(NSSelectorFromString("getModule:"), with: ExpoNativeAlipayModule.self)?.takeUnretainedValue() as? ExpoNativeAlipayModule {
+-                // 旧版
+-                module = m
+-            }
++    private func sendAlipayResult(_ resultDic: [AnyHashable: Any]?) {
++        guard let module = ExpoNativeAlipayModule.shared else {
++            print("[ExpoNativeAlipay] WARNING: Module.shared is nil, cannot resolve pay promise!")
++            return
+         }
+ 
+-        if let module = module {
+-            // 判断是支付还是授权,根据 resultDic 内容区分
+-            if let result = resultDic?["result"] as? String, result.contains("auth_code=") {
+-                module.handleAlipayAuthResult(resultDic ?? [:])
+-            } else {
+-                module.handleAlipayResult(resultDic ?? [:])
+-            }
++        // 判断是支付还是授权,根据 resultDic 内容区分
++        if let result = resultDic?["result"] as? String, result.contains("auth_code=") {
++            module.handleAlipayAuthResult(resultDic ?? [:])
++        } else {
++            module.handleAlipayResult(resultDic ?? [:])
+         }
+     }
+ }
+\ No newline at end of file
+diff --git a/node_modules/expo-native-alipay/ios/ExpoNativeAlipayModule.swift b/node_modules/expo-native-alipay/ios/ExpoNativeAlipayModule.swift
+index 2d925ec..a72ae4e 100644
+--- a/node_modules/expo-native-alipay/ios/ExpoNativeAlipayModule.swift
++++ b/node_modules/expo-native-alipay/ios/ExpoNativeAlipayModule.swift
+@@ -2,6 +2,7 @@ import ExpoModulesCore
+ import AlipaySDK
+ 
+ public class ExpoNativeAlipayModule: Module {
++  public static var shared: ExpoNativeAlipayModule?
+   private var alipayScheme: String?
+   private var payPromise: Promise?
+   private var authPromise: Promise?
+@@ -10,6 +11,11 @@ public class ExpoNativeAlipayModule: Module {
+ 
+   public func definition() -> ModuleDefinition {
+     Name("ExpoNativeAlipay")
++    
++    OnCreate {
++      ExpoNativeAlipayModule.shared = self
++    }
++    
+     /**
+     * 注册接口
+     *
+@@ -46,6 +52,17 @@ public class ExpoNativeAlipayModule: Module {
+       let version = AlipaySDK.defaultService()?.currentVersion() ?? ""
+       promise.resolve(version)
+     }
++
++    AsyncFunction("processOrder") { (urlStr: String, promise: Promise) in
++      if let url = URL(string: urlStr) {
++        AlipaySDK.defaultService()?.processOrder(withPaymentResult: url, standbyCallback: { resultDic in
++            self.handleAlipayResult(resultDic ?? [:])
++            promise.resolve(resultDic)
++        })
++      } else {
++        promise.resolve(nil)
++      }
++    }
+   }
+ 
+   // 处理支付结果
+diff --git a/node_modules/expo-native-alipay/src/ExpoNativeAlipay.types.ts b/node_modules/expo-native-alipay/src/ExpoNativeAlipay.types.ts
+index 0969717..310aeec 100644
+--- a/node_modules/expo-native-alipay/src/ExpoNativeAlipay.types.ts
++++ b/node_modules/expo-native-alipay/src/ExpoNativeAlipay.types.ts
+@@ -48,6 +48,11 @@ export interface OrderResultStr {
+   sign_type: 'RSA2' | 'RSA';
+ }
+ 
++/**
++ * 处理外部跳转回来的支付宝 URL (主要是为了给拦截了 URL 的框架手动处理用)
++ */
++export declare function processOrder(url: string): Promise<OrderResult>;
++
+ /**
+  * 支付订单返回结果
+  * @returns 成功返回
+diff --git a/node_modules/expo-native-alipay/src/ExpoNativeAlipayModule.ts b/node_modules/expo-native-alipay/src/ExpoNativeAlipayModule.ts
+index 2f22a8f..03dead7 100644
+--- a/node_modules/expo-native-alipay/src/ExpoNativeAlipayModule.ts
++++ b/node_modules/expo-native-alipay/src/ExpoNativeAlipayModule.ts
+@@ -60,6 +60,14 @@ export function getVersion(): Promise<string> {
+     return ExpoNativeAlipay.getVersion()
+ }
+ 
++/**
++ * 手动处理外部唤起返回的支付 URL
++ * @param url App 唤起的 URL
++ */
++export function processOrder(url: string): Promise<OrderResult> {
++    return ExpoNativeAlipay.processOrder(url)
++}
++
+ /**
+  * 设置支付宝跳转Scheme,仅 iOS
+  * @param scheme
+diff --git a/node_modules/expo-native-alipay/src/index.ts b/node_modules/expo-native-alipay/src/index.ts
+index 1a1b5d0..3e7f3cb 100644
+--- a/node_modules/expo-native-alipay/src/index.ts
++++ b/node_modules/expo-native-alipay/src/index.ts
+@@ -1,4 +1,5 @@
+ // Reexport the native module. On web, it will be resolved to ExpoNativeAlipayModule.web.ts
+ // and on native platforms to ExpoNativeAlipayModule.ts
+ export * from './ExpoNativeAlipay.types';
+-export { authInfo, default, getVersion, alipay, setAlipaySandbox, setAlipayScheme } from './ExpoNativeAlipayModule';
++export { alipay, authInfo, default, getVersion, processOrder, setAlipaySandbox, setAlipayScheme } from './ExpoNativeAlipayModule';
++