lv 1 kuukausi sitten
vanhempi
sitoutus
07db0171dc

+ 1 - 1
android/app/build.gradle

@@ -97,7 +97,7 @@ android {
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         versionCode 1
-        versionName "1.0.1"
+        versionName "1.0.2"
 
         buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
     }

+ 0 - 1
android/app/proguard-rules.pro

@@ -12,4 +12,3 @@
 -keep class com.facebook.react.turbomodule.** { *; }
 
 # Add any project specific keep options here:
-

+ 1 - 1
android/app/src/main/res/values/strings.xml

@@ -1,6 +1,6 @@
 <resources>
   <string name="app_name">Loan Assistant</string>
   <string name="expo_system_ui_user_interface_style" translatable="false">light</string>
-  <string name="expo_runtime_version">1.0.1</string>
+  <string name="expo_runtime_version">1.0.2</string>
   <string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
 </resources>

+ 1 - 1
app.json

@@ -2,7 +2,7 @@
   "expo": {
     "name": "Loan Assistant",
     "slug": "assistant",
-    "version": "1.0.1",
+    "version": "1.0.2",
     "orientation": "portrait",
     "icon": "./assets/images/icon.png",
     "scheme": "loanassistant",

BIN
assets/images/uploading.jpg


+ 1 - 1
ios/LoanAssistant/Info.plist

@@ -19,7 +19,7 @@
     <key>CFBundlePackageType</key>
     <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
     <key>CFBundleShortVersionString</key>
-    <string>1.0.1</string>
+    <string>1.0.2</string>
     <key>CFBundleSignature</key>
     <string>????</string>
     <key>CFBundleURLTypes</key>

+ 1 - 1
ios/LoanAssistant/Supporting/Expo.plist

@@ -36,7 +36,7 @@ FQ==&#xD;
     <key>EXUpdatesLaunchWaitMs</key>
     <integer>30000</integer>
     <key>EXUpdatesRuntimeVersion</key>
-    <string>1.0.1</string>
+    <string>1.0.2</string>
     <key>EXUpdatesURL</key>
     <string>https://updates-loan.ewaga.com/api/manifest</string>
   </dict>

+ 1 - 1
package.json

@@ -24,6 +24,7 @@
     "@react-navigation/native": "^7.1.33",
     "axios": "^1.14.0",
     "expo": "~55.0.12",
+    "expo-application": "~55.0.14",
     "expo-build-properties": "~55.0.13",
     "expo-constants": "~55.0.12",
     "expo-device": "~55.0.13",
@@ -45,7 +46,6 @@
     "react-native": "0.83.4",
     "react-native-gesture-handler": "~2.30.1",
     "react-native-mmkv": "^4.3.1",
-    "react-native-nitro-modules": "^0.35.3",
     "react-native-reanimated": "4.2.1",
     "react-native-safe-area-context": "~5.6.2",
     "react-native-screens": "~4.23.0",

+ 12 - 3
pnpm-lock.yaml

@@ -34,6 +34,9 @@ importers:
       expo:
         specifier: ~55.0.12
         version: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+      expo-application:
+        specifier: ~55.0.14
+        version: 55.0.14(expo@55.0.12)
       expo-build-properties:
         specifier: ~55.0.13
         version: 55.0.13(expo@55.0.12)
@@ -97,9 +100,6 @@ importers:
       react-native-mmkv:
         specifier: ^4.3.1
         version: 4.3.1(react-native-nitro-modules@0.35.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
-      react-native-nitro-modules:
-        specifier: ^0.35.3
-        version: 0.35.3(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
       react-native-reanimated:
         specifier: 4.2.1
         version: 4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
@@ -2648,6 +2648,11 @@ packages:
     resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==}
     engines: {node: '>=0.10.0'}
 
+  expo-application@55.0.14:
+    resolution: {integrity: sha512-NgqDIt3eCf4aVLp1L6AcEanCYoyJeuBsGrgGSzOIvxAsOvp5X3SYKW3ROgpKUnLQEKMWlzwETpjsUGszcqkk8g==}
+    peerDependencies:
+      expo: '*'
+
   expo-asset@55.0.13:
     resolution: {integrity: sha512-XDtshd8GZujYEmC84B3Gj+dCStvjcoywCyHrhO5K68J3CwkauIxyNeOLFlIX/U9FXtCuEykv14Lhz7xCcn1RWA==}
     peerDependencies:
@@ -8435,6 +8440,10 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  expo-application@55.0.14(expo@55.0.12):
+    dependencies:
+      expo: 55.0.12(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.9)(expo-router@55.0.11)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.16.0(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+
   expo-asset@55.0.13(expo@55.0.12)(react-native@0.83.4(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
     dependencies:
       '@expo/image-utils': 0.8.12

+ 139 - 45
src/app/_layout.tsx

@@ -1,18 +1,27 @@
+import { AnimatedSplashOverlay } from '@/components/animated-icon';
+import { antdTheme } from '@/constants/antd-theme';
+import { Colors } from '@/constants/theme';
 import '@/global.css';
+import api from '@/utils/api';
+import { AuthProvider } from '@/utils/auth';
+import { getGlobalStorage } from '@/utils/storage';
+import { Icon, Modal, Provider, Toast } from '@ant-design/react-native';
 import {
   DefaultTheme as ReactNavigationDefaultTheme,
   Theme,
   ThemeProvider,
 } from '@react-navigation/native';
-import * as updates from 'expo-updates';
-
-import { AnimatedSplashOverlay } from '@/components/animated-icon';
-import { antdTheme } from '@/constants/antd-theme';
-import { AuthProvider } from '@/utils/auth';
-import { Icon, Modal, Provider, Toast } from '@ant-design/react-native';
+import * as application from 'expo-application';
 import { useFonts } from 'expo-font';
+import { Image } from 'expo-image';
 import { Stack, useRouter } from 'expo-router';
+import * as SplashScreen from 'expo-splash-screen';
+import * as updates from 'expo-updates';
 import { useEffect, useState } from 'react';
+import { Linking, Platform } from 'react-native';
+
+SplashScreen.preventAutoHideAsync();
+
 // import * as app from 'expo-'
 export const DefaultTheme: Theme = {
   ...ReactNavigationDefaultTheme,
@@ -27,52 +36,131 @@ export const DefaultTheme: Theme = {
   },
 };
 
+
 export default function RootLayout() {
   const [initing, setIniting] = useState(true);
 
- 
+
   // const colorScheme = useColorScheme();
   const [fontsLoaded] = useFonts({
     antoutline: require('@ant-design/icons-react-native/fonts/antoutline.ttf'),
   });
+
   useEffect(() => {
-    
-    (async ()=> {
-      // try {
-      //   const appVersion = JSON.parse(getGlobalStorage().getString('app_version') || 'null')
-      //   if (!appVersion && appVersion.version && appVersion.version != expo)
-      //   const appVersion = api.get('common/check_version')
-      // }
-      try {
+    const now = Date.now() / 1000;
 
-      const res = await updates.checkForUpdateAsync();
-      // alert(JSON.stringify(res))
-      if (res.isAvailable) {
-
-        Modal.alert("提示", "发现新版本", [
-          {
-            text: '确认',
-            onPress: async ()=> {
-              const  l = Toast.loading("正在更新 ...");
-              try {
-                await updates.fetchUpdateAsync();
-                await updates.reloadAsync();
-              // eslint-disable-next-line @typescript-eslint/no-unused-vars
-              } catch(e) {
-                Modal.alert("提示", "更新遇到问题" );
+    async function checkApp() {
+      try {
+        const lastTime = parseInt(getGlobalStorage().getString('last_update_app') || '0');
+        if (now - lastTime < 900000) {
+          return true;
+        }
+SplashScreen.hide();
+      // setIniting(false);
+        const {
+          version,
+          package: pkg,
+          type,
+          force,
+        } = await api.post<{ version: string, package: string, type: 'apk' | 'url', force: boolean }>('common/check_version', {
+          platform: Platform.OS,
+          version: application.nativeApplicationVersion,
+        }, {timeout: 3000});
+        getGlobalStorage().set('last_update_app', now);
+        if (version === application.nativeApplicationVersion) {
+          return true;
+        }
+        const va = version.split('.');
+        let vb = (application.nativeApplicationVersion||'').split('.');
+        let update = false;
+        for (let i = 0; i < va.length; i++) {
+          if (vb.length <= i || parseInt(va[i]) > parseInt(vb[i])) {
+            update = true;
+            break;
+          }
+        }
+        if (!update) {
+          return true;
+        }
+        let ok = true;
+        do {
+          ok = await new Promise<boolean>((resolve, reject) => {
+            const actions: any[] = [
+              {
+                text: '立即更新',
+                onPress: async () => {
+                  if (type === 'url' || type === 'apk') {
+                    if (pkg) {
+                      if (await Linking.canOpenURL(pkg)) {
+                        Linking.openURL(pkg);
+                        resolve(false);
+                        return;
+                      }
+                    }
+                    Modal.alert('无法自动更新', "请前往应用商店更新");
+                    resolve(true);
+                    return;
+                  }
+                  resolve(true);
+                }
               }
-              Toast.remove(l);
+            ];
+            if (!force) {
+              actions.unshift({
+                text: '否',
+                style: { color: Colors.textSecondary },
+                onPress: () => {
+                  resolve(true);
+                }
+              });
             }
-          }
-        ], ()=>false);
+            Modal.alert(`${version}可更新`, force ? '请更新后继续使用' : '是否立即更新?', actions);
+          });
+        } while (!ok);
+        return true;
+      } catch (e) {
+        console.warn(e);
+        return true;
       }
-        
-      // eslint-disable-next-line @typescript-eslint/no-unused-vars
-      } catch(e) {
+    }
+
+    (async () => {
+      if (!await checkApp()) {
+        SplashScreen.hide();
+      setIniting(false);
+        return;
+      }
+
+      try {
+
+        const res = await updates.checkForUpdateAsync();
+        // alert(JSON.stringify(res))
+        if (res.isAvailable) {
+
+          Modal.alert("发现热更新", "需要立即下载", [
+            {
+              text: '确认',
+              onPress: async () => {
+                const l = Toast.loading("正在下载更新 ...");
+                try {
+                  await updates.fetchUpdateAsync();
+                  await updates.reloadAsync();
+                  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+                } catch (e) {
+                  Modal.alert("提示", "更新遇到问题");
+                }
+                Toast.remove(l);
+              }
+            }
+          ], () => false);
+        }
+
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      } catch (e) {
         // alert(e?.message || e+"");
       }
       setIniting(false);
-      
+
     })();
 
   }, []);
@@ -85,15 +173,21 @@ export default function RootLayout() {
     <ThemeProvider value={DefaultTheme}>
       <Provider theme={antdTheme}>
         {(!initing && fontsLoaded) &&
-        <AuthProvider>
-          <AnimatedSplashOverlay />
-          <Stack screenOptions={{ headerShown: false, headerTransparent: true,  headerLeft:({canGoBack})=>canGoBack&&<Icon name="arrow-left" onPress={router.back} />}}>
-            <Stack.Screen name="(tabs)" />
-            <Stack.Screen name="sign-in" />
-            <Stack.Screen name="web" options={{headerShown: true}} />
-          </Stack>
-        </AuthProvider>}
+          <AuthProvider>
+            <AnimatedSplashOverlay />
+            <Stack screenOptions={{
+                headerShown: false,
+                headerTransparent: true,
+                headerLeft: ({canGoBack}) => canGoBack && <Icon name="arrow-left" onPress={router.back} />,
+                animation: 'ios_from_right'
+                }}>
+              <Stack.Screen name="(tabs)" />
+              <Stack.Screen name="sign-in" options={{animation: 'fade_from_bottom'}} />
+              <Stack.Screen name="web" options={{ headerShown: true }} />
+            </Stack>
+          </AuthProvider>}
       </Provider>
+      {initing && <Image contentFit='contain' source={require('@/assets/images/uploading.jpg')} />}
     </ThemeProvider>
   );
 }

+ 35 - 43
src/app/sign-in.tsx

@@ -43,21 +43,18 @@ export default function SignInScreen() {
   const scrollView = useRef<ScrollView>(null);
 
   const [captchaVisible, setCaptchaVisible] = useState<boolean>(false);
-    const [smsTtl, setSmsTtl] = useState(0);
-  
-    
-    const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
-
+  const [smsTtl, setSmsTtl] = useState(0);
 
-    const needsCaptchaRef = useRef(false);
-    const handleCaptcha = (res: CaptchaRes) => {
-      let fun = waitCaptcha.current;
-      waitCaptcha.current = null;
   
-      setCaptchaVisible(false);
-      fun?.(res);
-    }
-    
+  const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
+  const needsCaptchaRef = useRef(false);
+  const handleCaptcha = (res: CaptchaRes) => {
+
+    let fun = waitCaptcha.current;
+    waitCaptcha.current = null;
+    setCaptchaVisible(false);
+    fun?.(res);
+  }
   const handleSendCode = async () => {
     if (mobile.trim().length !== 11) {
       Toast.fail('请先输入 11 位手机号');
@@ -65,16 +62,16 @@ export default function SignInScreen() {
     }
     let captcha: CaptchaRes = null!;
     while (true) {
-      try {
-        if (needsCaptchaRef.current) {
-          setCaptchaVisible(true);
-          captcha = await new Promise<CaptchaRes>((resolve) => {
-            waitCaptcha.current = resolve;
-          });
-          if (!captcha || !captcha.ok) {
-            return;
-          }
+      if (needsCaptchaRef.current) {
+        setCaptchaVisible(true);
+        captcha = await new Promise<CaptchaRes>((resolve) => {
+          waitCaptcha.current = resolve;
+        });
+        if (!captcha || !captcha.ok) {
+          return;
         }
+      }
+      try {
         const res = await api.post<{
           needsCaptcha?: boolean;
           timerout: number;
@@ -86,13 +83,6 @@ export default function SignInScreen() {
 
         if (res.needsCaptcha) {
           needsCaptchaRef.current = true;
-          setCaptchaVisible(true);
-          captcha = await new Promise<CaptchaRes>((resolve) => {
-            waitCaptcha.current = resolve;
-          });
-          if (!captcha || !captcha.ok) {
-            return;
-          }
           continue;
         }
         setSmsTtl(res.timerout);
@@ -100,6 +90,10 @@ export default function SignInScreen() {
 
       } catch (e) {
         if (ApiError.isApiError(e)) {
+          if (e.code === -1) {
+            Toast.fail("该手机已被注册");
+            return;
+          }
           if (e.code === -99) {
             Toast.fail("图形验证码无效");
             return;
@@ -112,8 +106,13 @@ export default function SignInScreen() {
   };
 
 
-  const handleLogin = async () => {
+  useInterval(() => {
+    setSmsTtl((pre) => pre - 1)
+  }, smsTtl > 0 ? 1000 : null);
 
+
+
+  const handleLogin = async () => {
     setFlushAgree(false);
     if (mobile.trim().length !== 11) {
       Toast.fail('请输入正确的手机号');
@@ -133,15 +132,13 @@ export default function SignInScreen() {
 
     if (!agreed) {
       Toast.fail('请先阅读并同意协议');
-      // scroll to agree
       setFlushAgree(true);
-      scrollView.current?.scrollToEnd({ animated: true });
+      // scrollView.current?.scrollToEnd({ animated: true });
       return;
     }
 
 
     setLoading(true);
-    const toastKey = Toast.loading('正在登录...');
 
     try {
       const token = authMode === 'sms' ? await smsSignIn(mobile, code) : await signIn(mobile, password);
@@ -158,15 +155,9 @@ export default function SignInScreen() {
       Toast.fail('登录失败,请稍后重试');
     } finally {
       setLoading(false);
-      Toast.remove(toastKey);
     }
   };
 
-   useInterval(() => {
-      setSmsTtl((pre) => pre - 1)
-    }, smsTtl > 0 ? 1000 : null);
-  
-    
   const insets = useSafeAreaInsets();
   return (
     <View className="flex-1 bg-surface">
@@ -178,7 +169,6 @@ export default function SignInScreen() {
           ref={scrollView}
           className="flex-1"
           contentContainerClassName="px-8"
-          contentInset={{ top: insets.top ?? 8, bottom: insets.bottom ?? 8 }}
           keyboardShouldPersistTaps="handled"
           showsVerticalScrollIndicator={false}
         >
@@ -186,7 +176,8 @@ export default function SignInScreen() {
           <View className="absolute top-32 -left-20 h-64 w-64 rounded-full bg-secondary-container/20" />
           <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
 
-          <View className="flex-1 justify-between">
+          <View className="flex-1 justify-between"
+            style={{ paddingTop: (insets.top ?? 10) + 52, paddingBottom: (insets.bottom ?? 8) + 24}}>
             <View>
               <View className="mb-6">
                 <View className="mb-3 h-16 w-16 items-center justify-center rounded-2xl bg-primary-container shadow-lg">
@@ -279,7 +270,7 @@ export default function SignInScreen() {
                         onPress={handleSendCode}
   
                       >
-                        <Text className={`text-lg font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed' : 'text-primary'}`}>
+                        <Text className={`text-lg w-22 text-center font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed/50' : 'text-primary'}`}>
                           {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
                         </Text>
                       </Pressable>
@@ -291,7 +282,7 @@ export default function SignInScreen() {
               </View>
 
               <View className="mb-6 gap-3">
-                <Button type='primary' onPress={handleLogin}>登录</Button>
+                <Button type='primary' loading={loading} onPress={handleLogin}>登录</Button>
 
                 <View className="flex-row items-center justify-between px-3">
                   <Pressable hitSlop={8} onPress={() => router.push({ pathname: '/sign-up', params: { signIn: 1, redirectTo } })}>
@@ -374,6 +365,7 @@ export default function SignInScreen() {
         </ScrollView>
       </KeyboardAvoidingView>
       <CaptchaBox visible={captchaVisible} onClose={handleCaptcha} />
+      {loading && <View className="absolute left-0 right-0 top-0 bottom-0 z-1 bg-white/5"  />}
     </View>
   );
 }

+ 36 - 59
src/app/sign-up.tsx

@@ -45,14 +45,13 @@ export default function SignUpScreen() {
   const { setToken } = useAuth();
 
   const { signIn, } = useLocalSearchParams<{ signIn: string; redirectTo?: string }>();
+  
   const waitCaptcha = useRef<(res: CaptchaRes) => void | null>(null);
-
   const needsCaptchaRef = useRef(false);
   const handleCaptcha = (res: CaptchaRes) => {
 
     let fun = waitCaptcha.current;
     waitCaptcha.current = null;
-alert(1)
     setCaptchaVisible(false);
     fun?.(res);
   }
@@ -84,15 +83,6 @@ alert(1)
 
         if (res.needsCaptcha) {
           needsCaptchaRef.current = true;
-          setCaptchaVisible(true);
-          captcha = await new Promise<CaptchaRes>((resolve) => {
-            waitCaptcha.current = resolve;
-          });
-
-        alert(JSON.stringify(captcha));
-          if (!captcha || !captcha.ok) {
-            return;
-          }
           continue;
         }
         setSmsTtl(res.timerout);
@@ -115,24 +105,13 @@ alert(1)
     }
   };
 
-  // useEffect(() => {
-  //   let isMount = true;
-  //   setTimeout(()=> {
-  //     if (!isMount) {
-  //       return;
-  //     }
-  //     setSmsTtl((pre)=> pre --);
-  //   })
-  //   return ()=> {
-  //     isMount = false;
-  //   }
-  // }, [])
 
   useInterval(() => {
     setSmsTtl((pre) => pre - 1)
   }, smsTtl > 0 ? 1000 : null);
 
   const handleRegister = async () => {
+    
     setFlushAgree(false);
 
     if (mobile.trim().length !== 11) {
@@ -168,7 +147,7 @@ alert(1)
 
     if (!agreed) {
       Toast.fail('请先阅读并同意协议');
-      setFlushAgree(true);
+      requestAnimationFrame(()=>setFlushAgree(true));
       scrollView.current?.scrollToEnd({ animated: true });
       return;
     }
@@ -208,8 +187,7 @@ alert(1)
           ref={scrollView}
           className="flex-1"
           contentContainerClassName="px-8"
-          contentInset={{ top: (insets.top ?? 10) + 2, bottom: (insets.bottom ?? 8) + 4 }}
-          contentContainerStyle={{ flexGrow: 1 }}
+          
           keyboardShouldPersistTaps="handled"
           showsVerticalScrollIndicator={false}
         >
@@ -217,7 +195,8 @@ alert(1)
           <View className="absolute top-32 -left-20 h-64 w-64 rounded-full bg-secondary-container/20" />
           <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
 
-          <View className="flex-1 justify-between">
+          <View className="flex-1 justify-between"
+              style={{ paddingTop: (insets.top ?? 10) + 2, paddingBottom: (insets.bottom ?? 8) + 24}}>
             <View>
               <View className="mb-12">
                 <View className="mb-6 h-16 w-16 items-center justify-center rounded-2xl bg-primary-container shadow-lg">
@@ -227,7 +206,7 @@ alert(1)
                   创建账号
                 </Text>
                 <Text className="text-base font-medium leading-7 text-on-surface-variant">
-                  完善基础信息,开启您的智能借贷助手工作台
+                  完善基础信息,成为助贷人,已有账号?<Text onPress={router.back} className="text-primary">立即登录</Text>
                 </Text>
               </View>
 
@@ -275,7 +254,7 @@ alert(1)
                       onPress={handleSendCode}
 
                     >
-                      <Text className={`text-lg font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed' : 'text-primary'}`}>
+                      <Text className={`text-lg w-22 text-center font-bold leading-6 ${loading || smsTtl > 0 ? 'text-on-secondary-fixed-variant' : 'text-primary'}`}>
                         {smsTtl > 0 ? `${smsTtl}s` : '发送验证码'}
                       </Text>
                     </Pressable>
@@ -373,37 +352,35 @@ alert(1)
               </View>
             </View>
 
-            <View className="pt-4">
-              <View className={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${flushAgree ? ' border-primary/50' : 'border-transparent'}`}>
-                <Pressable
-                  onPress={() => setAgreed((value) => !value)}
-                  hitSlop={8}
-                  className="pt-1"
+            <View className={`flex-row items-start gap-3 px-2 rounded-md border-2 transition-colors duration-700 ${flushAgree ? ' border-primary/50' : 'border-transparent'}`}>
+              <Pressable
+                onPress={() => setAgreed((value) => !value)}
+                hitSlop={8}
+                className="pt-1"
+              >
+                <View
+                  className={`h-6 w-6 mt-2 items-center justify-center border-2 border-primary/50 rounded-full cursor-pointer ${agreed
+                    ? 'border-primary bg-primary'
+                    : 'border-outline-variant bg-surface-container-low'
+                    }`}
                 >
-                  <View
-                    className={`h-6 w-6 mt-2 items-center justify-center border-2 border-primary/50 rounded-full cursor-pointer ${agreed
-                      ? 'border-primary bg-primary'
-                      : 'border-outline-variant bg-surface-container-low'
-                      }`}
-                  >
-                    {agreed ? (
-                      <Ionicons name="checkmark" size={14} color="#ffffff" />
-                    ) : null}
-                  </View>
-                </Pressable>
-                <Text className="flex-1 text-sm leading-6 text-on-surface-variant">
-                  注册即代表您已阅读并同意
-                  <Link href={{
-                    pathname: '/web',
-                    params: { uri: site }
-                  }}>
-                    <Text className="font-semibold text-primary">《用户服务协议》</Text>
-                  </Link>
-                  、
-                  <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《隐私政策》</Text>
-                  ,以及授权该应用获取您的公开信息。
-                </Text>
-              </View>
+                  {agreed ? (
+                    <Ionicons name="checkmark" size={14} color="#ffffff" />
+                  ) : null}
+                </View>
+              </Pressable>
+              <Text className="flex-1 text-sm leading-6 text-on-surface-variant">
+                注册即代表您已阅读并同意
+                <Link href={{
+                  pathname: '/web',
+                  params: { uri: site }
+                }}>
+                  <Text className="font-semibold text-primary">《用户服务协议》</Text>
+                </Link>
+                、
+                <Text className="font-semibold text-primary cursor-pointer" onPress={() => openBrowserAsync(`${site}`)}>《隐私政策》</Text>
+                ,以及授权该应用获取您的公开信息。
+              </Text>
             </View>
           </View>
         </ScrollView>

+ 22 - 21
src/utils/api.ts

@@ -25,15 +25,13 @@ export function getAccessToken() {
 
 
 
-export class HttpError {
+export class HttpError extends Error {
     public get data() {
         return this._data;
     }
-    name: string = 'ApiError';
-    constructor(private message: string, public code: number, private _data?: any) {
-        if (__DEV__) {
-            message = `${message} (code: ${code})`;
-        }
+    // name: string = 'HttpError';
+    constructor(message: string, public code: number, private _data?: any) {
+        super(`${code} ${message}`);
     }
 
     toString() {
@@ -60,7 +58,8 @@ export class HttpError {
 
 
 export class ApiError extends HttpError {
-
+     name: string = 'ApiError';
+     
 }
 
 
@@ -150,9 +149,12 @@ if (__DEV__) {
     );
 }
 
-
-async function request<T>(url: string, method: 'get' | 'post' | 'put' | 'delete', params: Record<string, any>, data: any): Promise<T> {
-    const headers: Record<string, string> = {};
+type Options = Omit<AxiosRequestConfig, 'headers'> & {headers?: Record<string, any>};
+async function request<T>(url: string, method: 'get' | 'post' | 'put' | 'delete', data?: any, config?: Options): Promise<T> {
+    const headers: Record<string, string> = {
+        ...config?.headers
+    };
+    
     let token = getAccessToken();
     if (token?.token) {
         headers['Authorization'] = `Bearer ${token.token}`;
@@ -161,8 +163,8 @@ async function request<T>(url: string, method: 'get' | 'post' | 'put' | 'delete'
         const response = await apiClient.request<ApiResponse<T>>({
             url,
             method,
-            params,
             data,
+            ...config,
             headers,
         });
         if (200 !== response.status) {
@@ -176,12 +178,11 @@ async function request<T>(url: string, method: 'get' | 'post' | 'put' | 'delete'
         }
         return res?.data as T;
     } catch (error) {
-        if (error instanceof ApiError) {
+        if (error instanceof ApiError || error instanceof HttpError) {
             throw error;
         }
-
         // @ts-ignore
-        throw new HttpError(error?.message || (`${error}`) || "unknown error", 500);
+        throw new HttpError(error?.message || (`${error}`) || "unknown error", NaN);
     }
 }
 const rawRequest = async <T>(req: AxiosRequestConfig) => {
@@ -210,19 +211,19 @@ const rawRequest = async <T>(req: AxiosRequestConfig) => {
 
 
 async function get<T>(api: string, params?: Record<string, any>): Promise<T> {
-    return await request<T>(api, 'get', params, undefined);
+    return await request<T>(api, 'get', undefined, {params});
 }
 
-async function post<T>(api: string, data?: any): Promise<T> {
-    return await request<T>(api, 'post', {}, data);
+async function post<T>(api: string, data?: any, config?: Options): Promise<T> {
+    return await request<T>(api, 'post', data, config);
 }
 
-async function put<T>(api: string, data?: any): Promise<T> {
-    return await request<T>(api, 'put', {}, data);
+async function put<T>(api: string, data?: any, config?: Options): Promise<T> {
+    return await request<T>(api, 'put', data, config);
 }
 
-async function deleted<T>(api: string): Promise<T> {
-    return await request<T>(api, 'delete', {}, undefined);
+async function deleted<T>(api: string, config?: Options): Promise<T> {
+    return await request<T>(api, 'delete', undefined, config);
 } const api = {
     post, get, put, deleted, request, rawRequest
 }

+ 11 - 2
src/utils/storage.ts

@@ -3,11 +3,20 @@ import * as fs from 'expo-file-system/legacy';
 
 import { createMMKV, MMKV } from 'react-native-mmkv';
 
+function fixPath(path: string) {
+    if (path.startsWith("file:///")) {
+        return path.substring(7);
+    }
+    if (path.startsWith("file:/")) {
+        return path.replace(/^file:\//, "/");
+    }
+}
 let globalStorage: MMKV | null = null;
 export function getGlobalStorage() {
     if (!globalStorage) {
         globalStorage = createMMKV({
             id: `global`,
+            path: fixPath(fs.cacheDirectory??''),
         });
     }
     return globalStorage;
@@ -18,7 +27,7 @@ export function getCaches() {
     if (!caches) {
         caches = createMMKV({
             id: `caches`,
-            path: fs.cacheDirectory!,
+            path: fixPath(fs.cacheDirectory??''),
         });
     }
     return caches;
@@ -29,7 +38,7 @@ export function getApiCache() {
     if (!apiCache) {
         apiCache = createMMKV({
             id: `api_cache`,
-            path: fs.cacheDirectory!,
+            path: fixPath(fs.cacheDirectory??''),
         });
     }
     return apiCache;