diff --git a/.kilocode/rules/memory-bank/context.md b/.kilocode/rules/memory-bank/context.md index aaaa371..37a7f9e 100644 --- a/.kilocode/rules/memory-bank/context.md +++ b/.kilocode/rules/memory-bank/context.md @@ -1,6 +1,7 @@ # 项目当前状态 ## 应用基本信息 + - **应用名称**: Out Live(超越生命) - **版本**: 1.0.19 - **Bundle ID**: com.anonymous.digitalpilates @@ -9,8 +10,9 @@ - **架构**: Expo Prebuild 后的 React Native 应用 ## 当前开发状态 + - **开发阶段**: 生产就绪版本 -- **最后更新**: 2025年10月 +- **最后更新**: 2025 年 11 月 - **主要功能**: 已完成核心健康数据追踪、AI 教练、目标管理、轻断食等功能 - **状态管理**: 使用 Redux Toolkit 进行状态管理 - **数据存储**: 本地使用 expo-sqlite/kv-store,远程 API 集成 @@ -18,41 +20,48 @@ ## 核心功能实现状态 ### 健康数据追踪 ✅ + - HealthKit 集成完成,支持步数、心率、HRV、睡眠等数据 - 活动圆环显示(活动卡路里、锻炼分钟、站立小时) - 实时健康数据监控和历史数据查看 - 健康权限管理系统 ### 营养管理 ✅ + - 饮食记录功能(文字、语音、拍照识别) - 营养成分分析和卡路里计算 - 食物库和自定义食物功能 - 营养标签识别 ### 目标与习惯管理 ✅ + - 目标创建、编辑、删除功能 - 任务分解和进度追踪 - 智能提醒系统 - 目标完成统计和分析 ### 轻断食功能 ✅ -- 多种预设断食方案(16:8、18:6等) + +- 多种预设断食方案(16:8、18:6 等) - 实时断食进度显示 - 断食提醒和通知 - 断食历史记录 ### AI 教练系统 ✅ + - AI 对话功能(流式响应) - 体态评估(照片分析) - 个性化健康建议 - 情绪分析(基于 HRV) ### 社区与挑战 ✅ + - 挑战赛参与和排行榜 - 成就系统 - 社交分享功能 ### 训练计划 ✅ + - 个性化训练计划生成 - 运动库和动作指导 - 训练进度记录 @@ -60,6 +69,7 @@ ## 技术架构状态 ### 前端架构 ✅ + - React Native 0.81.4 + Expo 54 - TypeScript 全面覆盖 - Expo Router 6.0 用于路由管理 @@ -67,12 +77,14 @@ - Liquid Glass 设计风格实现 ### 后端集成 ✅ + - RESTful API 集成(API 基础地址:https://pilate.richarjiang.com) - 用户认证和授权 - 数据同步和备份 - 推送通知服务 ### 原生功能 ✅ + - HealthKit 深度集成 - 推送通知(本地和远程) - 快捷动作(Quick Actions) @@ -82,32 +94,39 @@ ## 当前开发重点 ### 近期更新 -1. **性能优化**: 优化健康数据加载和图表渲染性能 -2. **用户体验**: 改进 Liquid Glass 设计效果和交互动画 -3. **数据同步**: 增强离线功能和数据同步稳定性 -4. **AI 功能**: 扩展 AI 教练对话能力和分析精度 + +1. **多语言支持**: 完善挑战页面的多语言翻译支持,建立翻译最佳实践指南 +2. **性能优化**: 优化健康数据加载和图表渲染性能 +3. **用户体验**: 改进 Liquid Glass 设计效果和交互动画 +4. **数据同步**: 增强离线功能和数据同步稳定性 +5. **AI 功能**: 扩展 AI 教练对话能力和分析精度 ### 待解决问题 -1. **测试覆盖**: 自动化测试覆盖率需要提升 -2. **错误监控**: 需要集成更完善的错误监控和分析 -3. **性能监控**: 应用性能监控和分析工具集成 -4. **文档完善**: API 文档和组件文档需要进一步完善 + +1. **多语言覆盖**: 其他页面的多语言翻译支持需要逐步完善 +2. **测试覆盖**: 自动化测试覆盖率需要提升 +3. **错误监控**: 需要集成更完善的错误监控和分析 +4. **性能监控**: 应用性能监控和分析工具集成 +5. **文档完善**: API 文档和组件文档需要进一步完善 ## 代码质量状态 ### 代码规范 ✅ + - ESLint 配置完善(eslint-config-expo) - Prettier 代码格式化 - TypeScript 严格模式 - 组件和函数命名规范 ### 项目结构 ✅ + - 清晰的目录结构(app/、components/、services/、store/、utils/) - 功能模块化组织 - 类型定义完整 - 常量和配置集中管理 ### 状态管理 ✅ + - Redux Toolkit 标准实现 - 异步操作处理规范 - 数据持久化策略 @@ -116,12 +135,14 @@ ## 部署和发布 ### 构建配置 ✅ + - Expo Prebuild 配置 - iOS 证书和配置文件 - App Store 发布配置 - 自动化构建流程 ### 发布状态 ✅ + - App Store 已发布版本 - 支持 OTA 更新 - 崩溃监控和分析 @@ -130,12 +151,14 @@ ## 团队协作 ### 开发工具 ✅ + - Git 版本控制 - VS Code 开发环境 - Expo 开发者工具 - iOS 模拟器和真机调试 ### 文档状态 🔄 + - API 文档部分完成 - 组件文档需要补充 - 部署文档完善 @@ -143,20 +166,24 @@ ## 下一步计划 -### 短期目标(1-2个月) -1. 完善自动化测试覆盖 -2. 优化应用启动性能 -3. 增强错误监控和分析 -4. 改进用户引导流程 +### 短期目标(1-2 个月) + +1. 完善所有核心页面的多语言翻译支持 +2. 完善自动化测试覆盖 +3. 优化应用启动性能 +4. 增强错误监控和分析 +5. 改进用户引导流程 + +### 中期目标(3-6 个月) -### 中期目标(3-6个月) 1. 扩展 AI 教练功能 2. 增加更多健康指标追踪 3. 优化数据同步策略 4. 增强社交功能 -### 长期目标(6个月以上) +### 长期目标(6 个月以上) + 1. 支持 Apple Watch 应用 2. 集成更多第三方健康设备 3. 开发 Web 端管理界面 -4. 扩展企业健康解决方案 \ No newline at end of file +4. 扩展企业健康解决方案 diff --git a/.kilocode/rules/memory-bank/tasks.md b/.kilocode/rules/memory-bank/tasks.md index dc720d0..cabba5b 100644 --- a/.kilocode/rules/memory-bank/tasks.md +++ b/.kilocode/rules/memory-bank/tasks.md @@ -5,26 +5,31 @@ **最后更新**: 2025-10-24 ### 重要规则 + **项目中不允许使用 MaterialIcons**,所有图标必须使用 Ionicons 以保持图标库的一致性。 ### 问题描述 + 在项目中发现使用 MaterialIcons 的情况,需要将所有 MaterialIcons 替换为 Ionicons,以保持图标库的一致性。 ### 解决方案 + 将所有 MaterialIcons 导入和使用替换为对应的 Ionicons。 ### 实现模式 #### 1. 替换导入语句 + ```typescript // ❌ 禁止使用 -import { MaterialIcons } from '@expo/vector-icons'; +import { MaterialIcons } from "@expo/vector-icons"; // ✅ 正确写法 -import { Ionicons } from '@expo/vector-icons'; +import { Ionicons } from "@expo/vector-icons"; ``` #### 2. 替换图标名称和属性 + ```typescript // ❌ 禁止使用 @@ -34,6 +39,7 @@ import { Ionicons } from '@expo/vector-icons'; ``` #### 3. 常见图标映射 + - `arrow-back-ios` → `chevron-back` (返回按钮) - `auto-awesome` → `star` (星星/自动推荐) - `tips-and-updates` → `bulb` (提示/建议) @@ -42,6 +48,7 @@ import { Ionicons } from '@expo/vector-icons'; - `remove` → `remove` (移除/删除,名称相同) ### 重要注意事项 + 1. **图标大小调整**:Ionicons 和 MaterialIcons 的默认大小可能不同,需要适当调整 2. **图标名称差异**:两个图标库的图标名称不同,需要找到对应的功能图标 3. **样式一致性**:确保替换后的图标在视觉上与原设计保持一致 @@ -49,6 +56,7 @@ import { Ionicons } from '@expo/vector-icons'; 5. **代码审查**:在代码审查中需要特别检查是否使用了 MaterialIcons ### 参考实现 + - `components/ui/HeaderBar.tsx` - 返回按钮的标准实现 - `components/model/MembershipModal.tsx` - 完整的 MaterialIcons 替换示例 @@ -57,21 +65,25 @@ import { Ionicons } from '@expo/vector-icons'; **最后更新**: 2025-10-24 ### 重要原则 + **所有按钮组件都需要尝试兼容 Liquid Glass**,这是项目的设计要求。 ### 实现模式 #### 1. 导入必要的组件 + ```typescript -import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; ``` #### 2. 检查设备支持情况 + ```typescript const isGlassAvailable = isLiquidGlassAvailable(); ``` #### 3. 实现条件渲染的按钮 + ```typescript @@ -96,26 +108,28 @@ const isGlassAvailable = isLiquidGlassAvailable(); ``` #### 4. 定义样式 + ```typescript const styles = StyleSheet.create({ button: { width: 40, height: 40, - borderRadius: 20, // 圆形按钮 - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', // 保证玻璃边界圆角效果 + borderRadius: 20, // 圆形按钮 + alignItems: "center", + justifyContent: "center", + overflow: "hidden", // 保证玻璃边界圆角效果 // 其他通用样式... }, fallbackButton: { - backgroundColor: 'rgba(255, 255, 255, 0.9)', + backgroundColor: "rgba(255, 255, 255, 0.9)", borderWidth: 1, - borderColor: 'rgba(255, 255, 255, 0.3)', + borderColor: "rgba(255, 255, 255, 0.3)", }, }); ``` ### 重要注意事项 + 1. **兼容性检查**:必须使用 `isLiquidGlassAvailable()` 检查设备支持情况 2. **overflow: 'hidden'**:GlassView 组件需要设置此属性以保证圆角效果 3. **降级样式**:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案 @@ -124,6 +138,7 @@ const styles = StyleSheet.create({ 6. **色调自定义**:通过 `tintColor` 属性自定义按钮的颜色主题 ### 常用配置 + - **glassEffectStyle**: "clear"(透明)或 "regular"(常规) - **tintColor**: 根据按钮功能选择合适的颜色 - 返回/导航操作:白色系 `rgba(255, 255, 255, 0.3)` @@ -132,6 +147,7 @@ const styles = StyleSheet.create({ - 信息操作:蓝色系 `rgba(33, 150, 243, 0.2)` ### 参考实现 + - `components/model/MembershipModal.tsx` - 悬浮返回按钮 - `components/glass/button.tsx` - 通用 Glass 按钮组件 - `app/(tabs)/_layout.tsx` - 标签栏按钮实现 @@ -141,24 +157,29 @@ const styles = StyleSheet.create({ **最后更新**: 2025-10-16 ### 问题描述 + 当使用 HeaderBar 组件时,需要正确处理内容区域的顶部距离,确保内容不会被状态栏或刘海屏遮挡。 ### 解决方案 + 使用 `useSafeAreaTop` hook 获取安全区域顶部距离,并应用到内容容器的样式中。 ### 实现模式 #### 1. 导入必要的 hook + ```typescript -import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; +import { useSafeAreaTop } from "@/hooks/useSafeAreaWithPadding"; ``` #### 2. 在组件中获取 safeAreaTop + ```typescript -const safeAreaTop = useSafeAreaTop() +const safeAreaTop = useSafeAreaTop(); ``` #### 3. 应用到内容容器 + ```typescript // 方式1: 直接应用到 View 组件 @@ -175,11 +196,13 @@ const safeAreaTop = useSafeAreaTop() ``` ### 重要注意事项 + 1. **不要在 StyleSheet 中使用变量**:不能在 `StyleSheet.create()` 中直接使用 `safeAreaTop` 变量 2. **使用动态样式**:必须通过内联样式或数组样式的方式动态应用 `safeAreaTop` 3. **不需要额外偏移**:通常只需要 `safeAreaTop`,不需要添加额外的固定像素值 ### 示例代码 + ```typescript // ❌ 错误写法 - 在 StyleSheet 中使用变量 const styles = StyleSheet.create({ @@ -193,6 +216,7 @@ const styles = StyleSheet.create({ ``` ### 参考页面 + - `app/steps/detail.tsx` - `app/water/detail.tsx` - `app/profile/goals.tsx` @@ -204,24 +228,29 @@ const styles = StyleSheet.create({ **最后更新**: 2025-10-16 ### 问题描述 + 在应用中实现符合 Liquid Glass 设计风格的图标按钮,需要考虑毛玻璃效果和兼容性处理。 ### 解决方案 + 使用 `GlassView` 组件实现毛玻璃效果,并提供不支持 Liquid Glass 的设备的降级方案。 ### 实现模式 #### 1. 导入必要的组件和函数 + ```typescript -import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; ``` #### 2. 检查设备支持情况 + ```typescript const isGlassAvailable = isLiquidGlassAvailable(); ``` #### 3. 实现条件渲染的按钮 + ```typescript @@ -246,25 +275,27 @@ const isGlassAvailable = isLiquidGlassAvailable(); ``` #### 4. 定义样式 + ```typescript const styles = StyleSheet.create({ glassButton: { width: 36, height: 36, - borderRadius: 18, // 圆形按钮 - alignItems: 'center', - justifyContent: 'center', - overflow: 'hidden', // 保证玻璃边界圆角效果 + borderRadius: 18, // 圆形按钮 + alignItems: "center", + justifyContent: "center", + overflow: "hidden", // 保证玻璃边界圆角效果 }, fallbackButton: { borderWidth: 1, - borderColor: 'rgba(244, 67, 54, 0.3)', - backgroundColor: 'rgba(244, 67, 54, 0.1)', + borderColor: "rgba(244, 67, 54, 0.3)", + backgroundColor: "rgba(244, 67, 54, 0.1)", }, }); ``` ### 重要注意事项 + 1. **兼容性处理**:必须使用 `isLiquidGlassAvailable()` 检查设备支持情况 2. **overflow: 'hidden'**:GlassView 组件需要设置此属性以保证圆角效果 3. **降级样式**:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案 @@ -272,6 +303,7 @@ const styles = StyleSheet.create({ 5. **色调自定义**:通过 `tintColor` 属性自定义按钮的颜色主题 ### 常用配置 + - **glassEffectStyle**: "clear"(透明)或 "regular"(常规) - **tintColor**: 根据按钮功能选择合适的颜色 - 删除操作:红色系 `rgba(244, 67, 54, 0.2)` @@ -279,6 +311,7 @@ const styles = StyleSheet.create({ - 信息操作:蓝色系 `rgba(33, 150, 243, 0.2)` ### 参考实现 + - `app/food/nutrition-analysis-history.tsx` - 删除按钮实现 - `components/glass/button.tsx` - 通用 Glass 按钮组件 - `app/(tabs)/_layout.tsx` - 标签栏按钮实现 @@ -288,27 +321,33 @@ const styles = StyleSheet.create({ **最后更新**: 2025-10-16 ### 问题描述 + 在应用中实现需要登录才能访问的功能时,需要判断用户是否已登录,未登录时先跳转到登录页面。 ### 解决方案 + 使用 `useAuthGuard` hook 中的 `pushIfAuthedElseLogin` 方法处理需要登录验证的导航操作,使用 `ensureLoggedIn` 方法处理需要登录验证的功能实现。 ### 权限校验原则 + **重要**: 功能实现如果包含服务端接口的调用,需要使用 `ensureLoggedIn` 来判断用户是否登录。 ### 实现模式 #### 1. 导入必要的 hook + ```typescript -import { useAuthGuard } from '@/hooks/useAuthGuard'; +import { useAuthGuard } from "@/hooks/useAuthGuard"; ``` #### 2. 在组件中获取方法 + ```typescript const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); ``` #### 3. 替换导航操作 + ```typescript // ❌ 原来的写法 - 没有登录验证 pushIfAuthedElseLogin('/food/nutrition-analysis-history')} - activeOpacity={0.7} - > - pushIfAuthedElseLogin("/food/nutrition-analysis-history")} + activeOpacity={0.7} + > + + + + + ) : ( + pushIfAuthedElseLogin("/food/nutrition-analysis-history")} + style={[styles.historyButton, styles.fallbackBackground]} + activeOpacity={0.7} > - - -) : ( - pushIfAuthedElseLogin('/food/nutrition-analysis-history')} - style={[styles.historyButton, styles.fallbackBackground]} - activeOpacity={0.7} - > - - -)} + + ); +} ``` ### 重要注意事项 + 1. **统一体验**:使用 `pushIfAuthedElseLogin` 可以确保登录后自动跳转到目标页面 2. **参数传递**:该方法支持传递路由参数,格式为 `pushIfAuthedElseLogin('/path', { param: value })` 3. **登录重定向**:登录页面会接收 `redirectTo` 和 `redirectParams` 参数用于登录后跳转 @@ -382,16 +426,19 @@ const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); 6. **异步处理**:`ensureLoggedIn` 是异步函数,需要使用 `await` 等待结果 ### 其他可用方法 + - `ensureLoggedIn()` - 检查登录状态,未登录时跳转到登录页面,返回布尔值表示是否已登录 - `guardHandler(fn, options)` - 包装一个函数,在执行前确保用户已登录 - `isLoggedIn` - 布尔值,表示当前用户是否已登录 ### 使用场景选择 + - **页面导航**:使用 `pushIfAuthedElseLogin` 处理页面跳转 - **服务端接口调用**:使用 `ensureLoggedIn` 验证登录状态后再执行功能 - **函数包装**:使用 `guardHandler` 包装需要登录验证的函数 ### 参考实现 + - `app/food/nutrition-label-analysis.tsx` - 成分表分析功能登录验证 - `app/(tabs)/personal.tsx` - 个人中心编辑按钮 - `hooks/useAuthGuard.ts` - 完整的认证守卫实现 @@ -401,39 +448,44 @@ const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard(); **最后更新**: 2025-10-16 ### 问题描述 + 在应用开发中,所有路由路径都应该使用常量定义,而不是硬编码字符串。这样可以确保路由的一致性,便于维护和重构。 ### 解决方案 + 将所有路由路径定义在 `constants/Routes.ts` 文件中,并在组件中使用这些常量。 ### 实现模式 #### 1. 添加新路由常量 + 在 `constants/Routes.ts` 文件中添加新的路由常量: ```typescript export const ROUTES = { // 现有路由... - + // 新增路由 - FOOD_CAMERA: '/food/camera', + FOOD_CAMERA: "/food/camera", } as const; ``` #### 2. 在组件中使用路由常量 + 导入并使用路由常量,而不是硬编码路径: ```typescript -import { ROUTES } from '@/constants/Routes'; +import { ROUTES } from "@/constants/Routes"; // ❌ 错误写法 - 硬编码路径 -router.push('/food/camera?mealType=dinner'); +router.push("/food/camera?mealType=dinner"); // ✅ 正确写法 - 使用路由常量 router.push(`${ROUTES.FOOD_CAMERA}?mealType=dinner`); ``` #### 3. 结合登录验证使用 + 对于需要登录验证的路由,结合 `pushIfAuthedElseLogin` 使用: ```typescript @@ -450,6 +502,7 @@ const { pushIfAuthedElseLogin } = useAuthGuard(); ``` ### 重要注意事项 + 1. **统一管理**:所有路由路径都必须在 `constants/Routes.ts` 中定义 2. **命名规范**:使用大写字母和下划线,如 `FOOD_CAMERA` 3. **路径一致性**:常量名应该清晰表达路由的用途 @@ -457,26 +510,244 @@ const { pushIfAuthedElseLogin } = useAuthGuard(); 5. **类型安全**:使用 `as const` 确保类型推导 ### 路由分类 + 按照功能模块对路由进行分组: ```typescript export const ROUTES = { // Tab路由 - TAB_EXPLORE: '/explore', - TAB_COACH: '/coach', - + TAB_EXPLORE: "/explore", + TAB_COACH: "/coach", + // 营养相关路由 - NUTRITION_RECORDS: '/nutrition/records', - FOOD_LIBRARY: '/food-library', - FOOD_CAMERA: '/food/camera', - + NUTRITION_RECORDS: "/nutrition/records", + FOOD_LIBRARY: "/food-library", + FOOD_CAMERA: "/food/camera", + // 用户相关路由 - AUTH_LOGIN: '/auth/login', - PROFILE_EDIT: '/profile/edit', + AUTH_LOGIN: "/auth/login", + PROFILE_EDIT: "/profile/edit", } as const; ``` ### 参考实现 + - `constants/Routes.ts` - 路由常量定义 - `components/NutritionRadarCard.tsx` - 使用路由常量和登录验证 -- `app/food/camera.tsx` - 食物拍照页面实现 \ No newline at end of file +- `app/food/camera.tsx` - 食物拍照页面实现 + +## 多语言翻译实现规范 + +**最后更新**: 2025-11-26 + +### 重要原则 + +**所有用户可见的文本都必须支持多语言翻译**,这是项目的基本要求。不允许在代码中硬编码任何用户可见的中文或英文文本。 + +### 问题描述 + +在开发新功能或修改现有功能时,所有用户界面文本都需要支持多语言切换,确保应用能够为不同语言用户提供本地化体验。 + +### 解决方案 + +使用项目集成的 i18next 翻译系统,在 `i18n/index.ts` 中定义翻译资源,在组件中使用 `useI18n` hook 获取翻译文本。 + +### 实现模式 + +#### 1. 导入必要的 hook + +```typescript +import { useI18n } from "@/hooks/useI18n"; +``` + +#### 2. 在组件中获取翻译函数 + +```typescript +const { t } = useI18n(); +``` + +#### 3. 添加翻译资源 + +在 `i18n/index.ts` 中为新的功能模块添加翻译资源: + +```typescript +// 中文翻译 +const newFeatureResources = { + title: "新功能标题", + subtitle: "新功能描述", + button: "按钮文本", + loading: "加载中...", + error: "操作失败,请稍后重试", + success: "操作成功", +}; + +// 英文翻译 +const newFeatureResourcesEn = { + title: "New Feature Title", + subtitle: "New feature description", + button: "Button Text", + loading: "Loading...", + error: "Operation failed, please try again later", + success: "Operation successful", +}; + +// 添加到资源对象中 +resources = { + zh: { + translation: { + // 现有翻译... + newFeature: newFeatureResources, + }, + }, + en: { + translation: { + // 现有翻译... + newFeature: newFeatureResourcesEn, + }, + }, +}; +``` + +#### 4. 在组件中使用翻译 + +```typescript +// ❌ 错误写法 - 硬编码文本 +加载中... +操作失败,请稍后重试 + +// ✅ 正确写法 - 使用翻译函数 +{t('newFeature.loading')} +{t('newFeature.error')} +``` + +#### 5. 动态参数翻译 + +对于包含动态参数的文本,使用插值语法: + +```typescript +// 翻译资源中 +welcome: '欢迎,{{name}}!' +itemsCount: '共 {{count}} 个项目' + +// 组件中使用 +{t('newFeature.welcome', { name: userName })} +{t('newFeature.itemsCount', { count: items.length })} +``` + +#### 6. 嵌套翻译键 + +对于复杂功能,使用嵌套的翻译键结构: + +```typescript +// 翻译资源 +modal: { + title: '确认操作', + description: '确定要执行此操作吗?', + buttons: { + confirm: '确认', + cancel: '取消', + }, +} + +// 组件中使用 +{t('newFeature.modal.title')} +{t('newFeature.modal.buttons.confirm')} +``` + +### 重要注意事项 + +1. **禁止硬编码**:所有用户可见的文本都必须通过翻译函数获取 +2. **完整翻译**:中文和英文翻译都必须提供,保持翻译完整性 +3. **语义化命名**:翻译键应该清晰表达文本的用途和含义 +4. **参数化文本**:包含动态内容的文本应该使用插值参数 +5. **一致性**:相同功能的文本应该使用相同的翻译键 +6. **Toast 消息**:Toast 提示消息也需要翻译支持 +7. **错误消息**:错误提示信息必须支持多语言 +8. **表单验证**:表单验证错误信息需要翻译 + +### 常见翻译模式 + +#### 1. 状态文本 + +```typescript +status: { + loading: '加载中...', + success: '操作成功', + error: '操作失败', + empty: '暂无数据', +} +``` + +#### 2. 按钮文本 + +```typescript +buttons: { + confirm: '确认', + cancel: '取消', + save: '保存', + delete: '删除', + edit: '编辑', + add: '添加', +} +``` + +#### 3. 表单相关 + +```typescript +form: { + placeholders: { + email: '请输入邮箱地址', + password: '请输入密码', + }, + errors: { + required: '此字段为必填项', + invalid: '格式不正确', + }, +} +``` + +#### 4. 列表和表格 + +```typescript +list: { + empty: '暂无数据', + loading: '加载中...', + loadMore: '加载更多', + refresh: '刷新', +} +``` + +### 翻译键命名规范 + +1. **使用小写字母和点号分隔**:`feature.section.item` +2. **按功能模块分组**:`challenges.title`, `challenges.subtitle` +3. **语义化命名**:`buttons.confirm`, `errors.network` +4. **避免缩写**:使用 `description` 而不是 `desc` + +### 参考实现 + +- `app/(tabs)/challenges.tsx` - 完整的多语言翻译实现示例 +- `i18n/index.ts` - 翻译资源配置 +- `hooks/useI18n.ts` - 翻译 hook 实现 +- `app/(tabs)/personal.tsx` - 个人中心页面翻译实现 +- `app/food/nutrition-label-analysis.tsx` - 营养分析页面翻译实现 + +### 检查清单 + +在开发新功能时,请确保: + +- [ ] 所有用户可见的文本都使用了翻译函数 +- [ ] 在 `i18n/index.ts` 中添加了对应的中文和英文翻译 +- [ ] Toast 消息支持多语言 +- [ ] 错误提示信息支持多语言 +- [ ] 表单验证错误信息支持多语言 +- [ ] 动态参数文本使用了插值语法 +- [ ] 翻译键命名符合规范 +- [ ] 测试了语言切换功能 + +### 最佳实践 + +1. **开发时即考虑多语言**:在编写组件时就使用翻译函数,而不是事后添加 +2. **保持翻译一致性**:相同含义的文本使用相同的翻译键 +3. **定期审查**:定期检查是否有硬编码文本遗漏 +4. **测试验证**:在开发完成后测试语言切换功能是否正常 diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index f487ad2..332387c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -99,7 +99,7 @@ export default function TabLayout() { color: colorTokens.tabIconSelected, fontSize: 12, fontWeight: '600', - marginLeft: 6, + marginLeft: 6 }} numberOfLines={1} > diff --git a/app/(tabs)/challenges.tsx b/app/(tabs)/challenges.tsx index 456b383..5f6f7d4 100644 --- a/app/(tabs)/challenges.tsx +++ b/app/(tabs)/challenges.tsx @@ -1,21 +1,32 @@ import dayjs from 'dayjs'; import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard'; +import { ConfirmationSheet } from '@/components/ui/ConfirmationSheet'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; +import { useI18n } from '@/hooks/useI18n'; import { fetchChallenges, + joinChallengeByCode, + resetJoinByCodeState, selectChallengeCards, selectChallengesListError, selectChallengesListStatus, + selectCustomChallengeCards, + selectJoinByCodeError, + selectJoinByCodeStatus, + selectOfficialChallengeCards, type ChallengeCardViewModel, } from '@/store/challengesSlice'; +import { Toast } from '@/utils/toast.utils'; +import { Ionicons } from '@expo/vector-icons'; +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from 'expo-router'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Animated, @@ -23,6 +34,7 @@ import { ScrollView, StyleSheet, Text, + TextInput, TouchableOpacity, View, useWindowDimensions @@ -32,11 +44,6 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; const AVATAR_SIZE = 36; const CARD_IMAGE_WIDTH = 132; const CARD_IMAGE_HEIGHT = 96; -const STATUS_LABELS: Record<'upcoming' | 'ongoing' | 'expired', string> = { - upcoming: '即将开始', - ongoing: '进行中', - expired: '已结束', -}; const CAROUSEL_ITEM_SPACING = 16; const MIN_CAROUSEL_CARD_WIDTH = 280; @@ -45,18 +52,32 @@ const DOT_BASE_SIZE = 6; export default function ChallengesScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const insets = useSafeAreaInsets(); + const { t } = useI18n(); - const { isLoggedIn } = useAuthGuard() + const { ensureLoggedIn } = useAuthGuard(); const colorTokens = Colors[theme]; const router = useRouter(); const dispatch = useAppDispatch(); - const challenges = useAppSelector(selectChallengeCards); + const glassAvailable = isLiquidGlassAvailable(); + const allChallenges = useAppSelector(selectChallengeCards); + const customChallenges = useAppSelector(selectCustomChallengeCards); + + + const officialChallenges = useAppSelector(selectOfficialChallengeCards); + const joinedCustomChallenges = useMemo( + () => customChallenges.filter((item) => item.isJoined), + [customChallenges] + ); const listStatus = useAppSelector(selectChallengesListStatus); const listError = useAppSelector(selectChallengesListError); + const joinByCodeStatus = useAppSelector(selectJoinByCodeStatus); + const joinByCodeError = useAppSelector(selectJoinByCodeError); + const [joinModalVisible, setJoinModalVisible] = useState(false); + const [shareCodeInput, setShareCodeInput] = useState(''); const ongoingChallenges = useMemo(() => { const now = dayjs(); - return challenges.filter((challenge) => { + return allChallenges.filter((challenge) => { if (challenge.status !== 'ongoing' || !challenge.isJoined || !challenge.progress) { return false; } @@ -70,7 +91,7 @@ export default function ChallengesScreen() { return true; }); - }, [challenges]); + }, [allChallenges]); const progressTrackColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : '#eceffa'; const progressInactiveColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.24)' : '#dfe4f6'; @@ -85,53 +106,132 @@ export default function ChallengesScreen() { ? ['#1f2230', '#10131e'] : [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd]; + useEffect(() => { + if (!joinModalVisible) { + dispatch(resetJoinByCodeState()); + setShareCodeInput(''); + } + }, [dispatch, joinModalVisible]); + + const handleCreatePress = useCallback(async () => { + const ok = await ensureLoggedIn(); + if (!ok) return; + router.push('/challenges/create-custom'); + }, [ensureLoggedIn, router]); + + const handleOpenJoin = useCallback(async () => { + const ok = await ensureLoggedIn(); + if (!ok) return; + setJoinModalVisible(true); + }, [ensureLoggedIn]); + + const isJoiningByCode = joinByCodeStatus === 'loading'; + + const handleSubmitShareCode = useCallback(async () => { + if (isJoiningByCode) return; + const ok = await ensureLoggedIn(); + if (!ok) return; + if (!shareCodeInput.trim()) { + Toast.warning(t('challenges.invalidInviteCode')); + return; + } + const formatted = shareCodeInput.trim().toUpperCase(); + try { + const result = await dispatch(joinChallengeByCode(formatted)).unwrap(); + await dispatch(fetchChallenges()); + setJoinModalVisible(false); + Toast.success(t('challenges.joinSuccess')); + router.push({ pathname: '/challenges/[id]', params: { id: result.challenge.id } }); + } catch (error) { + const message = typeof error === 'string' ? error : t('challenges.joinFailed'); + Toast.error(message); + } + }, [dispatch, ensureLoggedIn, isJoiningByCode, router, shareCodeInput]); + const renderChallenges = () => { - if (listStatus === 'loading' && challenges.length === 0) { + if (listStatus === 'loading' && allChallenges.length === 0) { return ( - 加载挑战中… + {t('challenges.loading')} ); } - if (listStatus === 'failed' && challenges.length === 0) { + if (listStatus === 'failed' && allChallenges.length === 0) { return ( - {listError ?? '加载挑战失败,请稍后重试'} + {listError ?? t('challenges.loadFailed')} dispatch(fetchChallenges())} > - 重新加载 + {t('challenges.retry')} ); } - if (challenges.length === 0) { + if (customChallenges.length === 0 && officialChallenges.length === 0) { return ( - 暂无挑战,稍后再来探索。 + {t('challenges.empty')} ); } - return challenges.map((challenge) => ( - - router.push({ pathname: '/challenges/[id]', params: { id: challenge.id } }) - } - /> - )); + return ( + + {joinedCustomChallenges.length ? ( + <> + + {t('challenges.customChallenges')} + + + {joinedCustomChallenges.map((challenge) => ( + + router.push({ pathname: '/challenges/[id]', params: { id: challenge.id } }) + } + /> + ))} + + + ) : null} + + + {t('challenges.officialChallengesTitle')} + + {officialChallenges.length ? ( + + {officialChallenges.map((challenge) => ( + + router.push({ pathname: '/challenges/[id]', params: { id: challenge.id } }) + } + /> + ))} + + ) : ( + + {t('challenges.officialChallenges')} + + )} + + ); }; return ( @@ -146,19 +246,42 @@ export default function ChallengesScreen() { > - 挑战 - 参与精选活动,保持每日动力 + {t('challenges.title')} + {t('challenges.subtitle')} + + + + {glassAvailable ? ( + + {t('challenges.join')} + + ) : ( + + {t('challenges.join')} + + )} + + + {glassAvailable ? ( + + + + ) : ( + + + + )} + - {/* - - - - */} {ongoingChallenges.length ? ( @@ -175,6 +298,34 @@ export default function ChallengesScreen() { {renderChallenges()} + setJoinModalVisible(false)} + onConfirm={handleSubmitShareCode} + title={t('challenges.joinModal.title')} + description={t('challenges.joinModal.description')} + confirmText={isJoiningByCode ? t('challenges.joinModal.joining') : t('challenges.joinModal.confirm')} + cancelText={t('challenges.joinModal.cancel')} + loading={isJoiningByCode} + content={ + + setShareCodeInput(text.toUpperCase())} + autoCapitalize="characters" + autoCorrect={false} + keyboardType="default" + maxLength={12} + /> + {joinByCodeError && joinModalVisible ? ( + {joinByCodeError} + ) : null} + + } + /> ); } @@ -188,7 +339,8 @@ type ChallengeCardProps = { }; function ChallengeCard({ challenge, surfaceColor, textColor, mutedColor, onPress }: ChallengeCardProps) { - const statusLabel = STATUS_LABELS[challenge.status] ?? challenge.status; + const { t } = useI18n(); + const statusLabel = t(`challenges.statusLabels.${challenge.status}`) ?? challenge.status; return ( {challenge.participantsLabel} - {challenge.isJoined ? ' · 已加入' : ''} + {challenge.isJoined ? ` · ${t('challenges.joined')}` : ''} {challenge.avatars.length ? ( @@ -328,7 +480,7 @@ function OngoingChallengesCarousel({ > { + const progressRecord = challenge?.progress as { lastProgressAt?: string; last_progress_at?: string } | undefined; + return progressRecord?.lastProgressAt ?? progressRecord?.last_progress_at; + }, [challenge?.progress]); + const hasCheckedInToday = useMemo(() => { + if (!challenge?.progress) { + return false; + } + if (lastProgressAt) { + const lastDate = dayjs(lastProgressAt); + if (lastDate.isValid() && lastDate.isSame(dayjs(), 'day')) { + return true; + } + } + return challenge.progress.checkedInToday ?? false; + }, [challenge?.progress, lastProgressAt]); const rankingData = useMemo(() => { const source = rankingList?.items ?? challenge?.rankings ?? []; @@ -165,6 +189,7 @@ export default function ChallengeDetailScreen() { () => rankingData.filter((item) => item.avatar).map((item) => item.avatar as string).slice(0, 6), [rankingData], ); + const showShareCode = isJoined && Boolean(challenge?.shareCode); const handleViewAllRanking = () => { if (!id) { @@ -192,7 +217,7 @@ export default function ChallengeDetailScreen() { try { Toast.show({ type: 'info', - text1: '正在生成分享卡片...', + text1: t('challengeDetail.share.generating'), }); // 捕获分享卡片视图 @@ -203,8 +228,8 @@ export default function ChallengeDetailScreen() { // 分享图片 const shareMessage = isJoined && progress - ? `我正在参与「${challenge.title}」挑战,已完成 ${progress.completed}/${progress.target} 天!一起加入吧!` - : `发现一个很棒的挑战「${challenge.title}」,一起来参与吧!`; + ? t('challengeDetail.share.messageJoined', { title: challenge.title, completed: progress.completed, target: progress.target }) + : t('challengeDetail.share.messageNotJoined', { title: challenge.title }); await Share.share({ title: challenge.title, @@ -213,7 +238,7 @@ export default function ChallengeDetailScreen() { }); } catch (error) { console.warn('分享失败', error); - Toast.error('分享失败,请稍后重试'); + Toast.error(t('challengeDetail.share.failed')); } }; @@ -234,7 +259,7 @@ export default function ChallengeDetailScreen() { await dispatch(fetchChallengeRankings({ id })); setShowCelebration(true) } catch (error) { - Toast.error('加入挑战失败') + Toast.error(t('challengeDetail.alert.joinFailed')) } }; @@ -246,7 +271,7 @@ export default function ChallengeDetailScreen() { await dispatch(leaveChallenge(id)).unwrap(); await dispatch(fetchChallengeDetail(id)).unwrap(); } catch (error) { - Toast.error('退出挑战失败'); + Toast.error(t('challengeDetail.alert.leaveFailed')); } }; @@ -254,34 +279,76 @@ export default function ChallengeDetailScreen() { if (!id || leaveStatus === 'loading') { return; } - Alert.alert('确认退出挑战?', '退出后需要重新加入才能继续坚持。', [ - { text: '取消', style: 'cancel' }, - { - text: '退出挑战', - style: 'destructive', - onPress: () => { - void handleLeave(); + Alert.alert( + t('challengeDetail.alert.leaveConfirm.title'), + t('challengeDetail.alert.leaveConfirm.message'), + [ + { text: t('challengeDetail.alert.leaveConfirm.cancel'), style: 'cancel' }, + { + text: t('challengeDetail.alert.leaveConfirm.confirm'), + style: 'destructive', + onPress: () => { + void handleLeave(); + }, }, - }, - ]); + ] + ); }; - const handleProgressReport = () => { + const handleProgressReport = async () => { if (!id || progressStatus === 'loading') { return; } - dispatch(reportChallengeProgress({ id })); + + if (hasCheckedInToday) { + Toast.info(t('challengeDetail.checkIn.toast.alreadyChecked')); + return; + } + + if (challenge?.status === 'upcoming') { + Toast.info(t('challengeDetail.checkIn.toast.notStarted')); + return; + } + + if (challenge?.status === 'expired') { + Toast.info(t('challengeDetail.checkIn.toast.expired')); + return; + } + + const isLoggedIn = await ensureLoggedIn(); + if (!isLoggedIn) { + return; + } + + if (!isJoined) { + Toast.info(t('challengeDetail.checkIn.toast.mustJoin')); + return; + } + + try { + await dispatch(reportChallengeProgress({ id, value: 1 })).unwrap(); + Toast.success(t('challengeDetail.checkIn.toast.success')); + } catch (error) { + Toast.error(t('challengeDetail.checkIn.toast.failed')); + } + }; + + const handleCopyShareCode = async () => { + if (!challenge?.shareCode) return; + await Clipboard.setStringAsync(challenge.shareCode); + // 添加震动反馈 + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + Toast.success(t('challengeDetail.shareCode.copied')); }; - const isJoined = challenge?.isJoined ?? false; const isLoadingInitial = detailStatus === 'loading' && !challenge; if (!id) { return ( - router.back()} withSafeTop transparent={false} /> + router.back()} withSafeTop transparent={false} /> - 未找到该挑战,稍后再试试吧。 + {t('challengeDetail.notFound')} ); @@ -290,10 +357,10 @@ export default function ChallengeDetailScreen() { if (isLoadingInitial) { return ( - router.back()} withSafeTop transparent={false} /> + router.back()} withSafeTop transparent={false} /> - 加载挑战详情中… + {t('challengeDetail.loading')} ); @@ -302,43 +369,43 @@ export default function ChallengeDetailScreen() { if (!challenge) { return ( - router.back()} withSafeTop transparent={false} /> + router.back()} withSafeTop transparent={false} /> - {detailError ?? '未找到该挑战,稍后再试试吧。'} + {detailError ?? t('challengeDetail.notFound')} dispatch(fetchChallengeDetail(id))} > - 重新加载 + {t('challengeDetail.retry')} ); } - const highlightTitle = challenge.highlightTitle ?? '立即加入挑战'; - const highlightSubtitle = challenge.highlightSubtitle ?? '邀请好友一起坚持,更容易收获成果'; - const joinCtaLabel = joinStatus === 'loading' ? '加入中…' : challenge.ctaLabel ?? '立即加入挑战'; + const highlightTitle = challenge.highlightTitle ?? t('challengeDetail.highlight.join.title'); + const highlightSubtitle = challenge.highlightSubtitle ?? t('challengeDetail.highlight.join.subtitle'); + const joinCtaLabel = joinStatus === 'loading' ? t('challengeDetail.cta.joining') : challenge.ctaLabel ?? t('challengeDetail.cta.join'); const isUpcoming = challenge.status === 'upcoming'; const isExpired = challenge.status === 'expired'; const upcomingStartLabel = formatMonthDay(challenge.startAt); - const upcomingHighlightTitle = '挑战即将开始'; + const upcomingHighlightTitle = t('challengeDetail.highlight.upcoming.title'); const upcomingHighlightSubtitle = upcomingStartLabel - ? `${upcomingStartLabel} 开始,敬请期待` - : '挑战即将开启,敬请期待'; - const upcomingCtaLabel = '挑战即将开始'; + ? t('challengeDetail.highlight.upcoming.subtitle', { date: upcomingStartLabel }) + : t('challengeDetail.highlight.upcoming.subtitleFallback'); + const upcomingCtaLabel = t('challengeDetail.cta.upcoming'); const expiredEndLabel = formatMonthDay(challenge.endAt); - const expiredHighlightTitle = '挑战已结束'; + const expiredHighlightTitle = t('challengeDetail.highlight.expired.title'); const expiredHighlightSubtitle = expiredEndLabel - ? `${expiredEndLabel} 已截止,期待下一次挑战` - : '本轮挑战已结束,期待下一次挑战'; - const expiredCtaLabel = '挑战已结束'; - const leaveHighlightTitle = '先别急着离开'; - const leaveHighlightSubtitle = '再坚持一下,下一个里程碑就要出现了'; - const leaveCtaLabel = leaveStatus === 'loading' ? '退出中…' : '退出挑战'; + ? t('challengeDetail.highlight.expired.subtitle', { date: expiredEndLabel }) + : t('challengeDetail.highlight.expired.subtitleFallback'); + const expiredCtaLabel = t('challengeDetail.cta.expired'); + const leaveHighlightTitle = t('challengeDetail.highlight.leave.title'); + const leaveHighlightSubtitle = t('challengeDetail.highlight.leave.subtitle'); + const leaveCtaLabel = leaveStatus === 'loading' ? t('challengeDetail.cta.leaving') : t('challengeDetail.cta.leave'); let floatingHighlightTitle = highlightTitle; let floatingHighlightSubtitle = highlightSubtitle; @@ -349,8 +416,10 @@ export default function ChallengeDetailScreen() { let isDisabledButtonState = false; if (isJoined) { - floatingHighlightTitle = leaveHighlightTitle; - floatingHighlightSubtitle = leaveHighlightSubtitle; + floatingHighlightTitle = showShareCode + ? `分享码 ${challenge?.shareCode ?? ''}` + : leaveHighlightTitle; + floatingHighlightSubtitle = showShareCode ? '' : leaveHighlightSubtitle; floatingCtaLabel = leaveCtaLabel; floatingOnPress = handleLeaveConfirm; floatingDisabled = leaveStatus === 'loading'; @@ -380,6 +449,23 @@ export default function ChallengeDetailScreen() { const participantsLabel = formatParticipantsLabel(challenge.participantsCount); const inlineErrorMessage = detailStatus === 'failed' && detailError ? detailError : undefined; + const checkInDisabled = + progressStatus === 'loading' || hasCheckedInToday || !isJoined || isUpcoming || isExpired; + const checkInButtonLabel = + progressStatus === 'loading' + ? t('challengeDetail.checkIn.button.checking') + : hasCheckedInToday + ? t('challengeDetail.checkIn.button.checked') + : !isJoined + ? t('challengeDetail.checkIn.button.notJoined') + : isUpcoming + ? t('challengeDetail.checkIn.button.upcoming') + : isExpired + ? t('challengeDetail.checkIn.button.expired') + : t('challengeDetail.checkIn.button.checkIn'); + const checkInSubtitle = hasCheckedInToday + ? t('challengeDetail.checkIn.subtitleChecked') + : t('challengeDetail.checkIn.subtitle'); return ( @@ -411,9 +497,9 @@ export default function ChallengeDetailScreen() { // 已加入:显示个人进度 - 我的坚持进度 + {t('challengeDetail.shareCard.progress.label')} - {progress.completed} / {progress.target} 天 + {t('challengeDetail.shareCard.progress.days', { completed: progress.completed, target: progress.target })} @@ -429,8 +515,8 @@ export default function ChallengeDetailScreen() { {progress.completed === progress.target - ? '🎉 已完成挑战!' - : `还差 ${progress.target - progress.completed} 天完成挑战`} + ? t('challengeDetail.shareCard.progress.completed') + : t('challengeDetail.shareCard.progress.remaining', { remaining: progress.target - progress.completed })} ) : ( @@ -454,7 +540,7 @@ export default function ChallengeDetailScreen() { {challenge.requirementLabel} - 按日打卡自动累计 + {t('challengeDetail.shareCard.info.checkInDaily')} @@ -464,7 +550,7 @@ export default function ChallengeDetailScreen() { {participantsLabel} - 快来一起坚持吧 + {t('challengeDetail.shareCard.info.joinUs')} @@ -472,7 +558,7 @@ export default function ChallengeDetailScreen() { {/* 底部标识 */} - Out Live · 超越生命 + {t('challengeDetail.shareCard.footer')} @@ -568,7 +654,7 @@ export default function ChallengeDetailScreen() { {challenge.requirementLabel} - 按日打卡自动累计 + {t('challengeDetail.detail.requirement')} @@ -590,19 +676,50 @@ export default function ChallengeDetailScreen() { ))} {challenge.participantsCount && challenge.participantsCount > participantAvatars.length ? ( - 更多 + {t('challengeDetail.participants.more')} ) : null} ) : null} + + {isCustomChallenge ? ( + + + {hasCheckedInToday ? t('challengeDetail.checkIn.todayChecked') : t('challengeDetail.checkIn.title')} + {checkInSubtitle} + + + + + {checkInButtonLabel} + + + + + ) : null} - 排行榜 + {t('challengeDetail.ranking.title')} - 查看全部 + {t('challengeDetail.detail.viewAllRanking')} @@ -623,7 +740,7 @@ export default function ChallengeDetailScreen() { )) ) : ( - 榜单即将开启,快来抢占席位。 + {t('challengeDetail.ranking.empty')} )} @@ -632,11 +749,30 @@ export default function ChallengeDetailScreen() { - - {floatingHighlightTitle} - {floatingHighlightSubtitle} - {floatingError ? {floatingError} : null} - + {showShareCode ? ( + + + {floatingHighlightTitle} + + + + + {floatingHighlightSubtitle ? ( + {floatingHighlightSubtitle} + ) : null} + {floatingError ? {floatingError} : null} + + ) : ( + + {floatingHighlightTitle} + {floatingHighlightSubtitle} + {floatingError ? {floatingError} : null} + + )} dayjs().startOf('day').toDate(), []); + const defaultEnd = useMemo(() => dayjs().add(21, 'day').startOf('day').toDate(), []); + + const [title, setTitle] = useState(''); + const [image, setImage] = useState(FALLBACK_IMAGE); + const [imagePreview, setImagePreview] = useState(null); + const { upload, uploading } = useCosUpload({ prefix: 'images/challenges' }); + const [type, setType] = useState(ChallengeType.WATER); + const [startDate, setStartDate] = useState(today); + const [endDate, setEndDate] = useState(defaultEnd); + const [targetValue, setTargetValue] = useState(''); + const [minimumCheckInDays, setMinimumCheckInDays] = useState(''); + const [requirementLabel, setRequirementLabel] = useState(''); + const [summary, setSummary] = useState(''); + const [progressUnit] = useState('天'); + const [periodLabel, setPeriodLabel] = useState(''); + const [periodEdited, setPeriodEdited] = useState(false); + const [rankingDescription] = useState('连续打卡榜'); + const [isPublic, setIsPublic] = useState(true); + const [maxParticipants, setMaxParticipants] = useState('100'); + const [minimumEdited, setMinimumEdited] = useState(false); + + const [shareCode, setShareCode] = useState(null); + const [shareModalVisible, setShareModalVisible] = useState(false); + const [createdChallengeId, setCreatedChallengeId] = useState(null); + + const [pickerType, setPickerType] = useState(null); + + const durationDays = useMemo( + () => + Math.max( + 1, + dayjs(endDate).startOf('day').diff(dayjs(startDate).startOf('day'), 'day') + 1 + ), + [startDate, endDate] + ); + const durationLabel = useMemo(() => `持续${durationDays}天`, [durationDays]); + + useEffect(() => { + if (!periodEdited) { + setPeriodLabel(`${durationDays}天挑战`); + } + if (!minimumEdited) { + setMinimumCheckInDays(String(durationDays)); + } + }, [durationDays, minimumEdited, periodEdited]); + + const handleConfirmDate = (date: Date) => { + if (!pickerType) return; + const normalized = dayjs(date).startOf('day'); + if (pickerType === 'start') { + const nextStart = normalized.isAfter(dayjs(), 'day') + ? normalized + : dayjs().add(1, 'day').startOf('day'); + setStartDate(nextStart.toDate()); + if (dayjs(endDate).isSameOrBefore(nextStart)) { + const nextEnd = nextStart.add(20, 'day').toDate(); + setEndDate(nextEnd); + } + } else { + const minEnd = dayjs(startDate).add(1, 'day').startOf('day'); + const nextEnd = normalized.isAfter(minEnd) ? normalized : minEnd; + setEndDate(nextEnd.toDate()); + } + setPickerType(null); + }; + + const handleSubmit = async () => { + if (isCreating) return; + if (!title.trim()) { + Toast.warning('请填写挑战标题'); + return; + } + + if (!requirementLabel.trim()) { + Toast.warning('请填写挑战要求说明'); + return; + } + + const startTimestamp = dayjs(startDate).valueOf(); + const endTimestamp = dayjs(endDate).valueOf(); + if (endTimestamp <= startTimestamp) { + Toast.warning('结束时间需要晚于开始时间'); + return; + } + + const target = Number(targetValue); + if (!Number.isFinite(target) || target < 1 || target > 1000) { + Toast.warning('每日目标值需在 1-1000 之间'); + return; + } + + const minDays = Number(minimumCheckInDays) || durationDays; + if (!Number.isFinite(minDays) || minDays < 1 || minDays > 365) { + Toast.warning('最少打卡天数需在 1-365 之间'); + return; + } + if (minDays > durationDays) { + Toast.warning('最少打卡天数不能超过持续天数'); + return; + } + + const maxP = maxParticipants ? Number(maxParticipants) : null; + if (maxP !== null && (!Number.isFinite(maxP) || maxP < 2 || maxP > 10000)) { + Toast.warning('参与人数需在 2-10000 之间,或留空表示无限制'); + return; + } + + const safeTitle = title.trim() || '自定义挑战'; + const payload: CreateCustomChallengePayload = { + title: safeTitle, + type, + image: image?.trim() || undefined, + startAt: startTimestamp, + endAt: endTimestamp, + targetValue: target, + minimumCheckInDays: minDays, + durationLabel, + requirementLabel: requirementLabel.trim() || '请填写挑战要求', + summary: summary.trim() || undefined, + progressUnit: progressUnit.trim() || '天', + periodLabel: periodLabel.trim() || undefined, + rankingDescription: rankingDescription.trim() || undefined, + isPublic, + maxParticipants: maxP, + }; + + try { + const created = await dispatch(createCustomChallengeThunk(payload)).unwrap(); + setShareCode(created.shareCode ?? null); + setCreatedChallengeId(created.id); + setShareModalVisible(true); + Toast.success('自定义挑战已创建'); + dispatch(fetchChallenges()); + } catch (error) { + const message = typeof error === 'string' ? error : '创建失败,请稍后再试'; + Toast.error(message); + } + }; + + const handleCopyShareCode = async () => { + if (!shareCode) return; + await Clipboard.setStringAsync(shareCode); + Toast.success('邀请码已复制'); + }; + + const handleTargetInputChange = (value: string) => { + const digits = value.replace(/\D/g, ''); + if (!digits) { + setTargetValue(''); + return; + } + const num = Math.min(1000, parseInt(digits, 10)); + setTargetValue(String(num)); + }; + + const handleMinimumDaysChange = (value: string) => { + const digits = value.replace(/\D/g, ''); + if (!digits) { + setMinimumCheckInDays(''); + setMinimumEdited(true); + return; + } + const num = Math.max(1, Math.min(365, parseInt(digits, 10))); + if (num > durationDays) { + setMinimumCheckInDays(String(durationDays)); + setMinimumEdited(true); + return; + } + setMinimumEdited(true); + setMinimumCheckInDays(String(num)); + }; + + const handlePickImage = useCallback(() => { + Alert.alert( + '选择封面图', + '请选择封面来源', + [ + { + text: '拍照', + onPress: async () => { + try { + const permission = await ImagePicker.requestCameraPermissionsAsync(); + if (permission.status !== 'granted') { + Alert.alert('权限不足', '需要相机权限以拍摄封面'); + return; + } + const result = await ImagePicker.launchCameraAsync({ + allowsEditing: true, + quality: 0.6, + aspect: [16, 9], + }); + if (result.canceled || !result.assets?.length) return; + const asset = result.assets[0]; + setImagePreview(asset.uri); + setImage(undefined); + try { + const { url } = await upload( + { + uri: asset.uri, + name: asset.fileName ?? `challenge-${Date.now()}.jpg`, + type: asset.mimeType ?? 'image/jpeg', + }, + { prefix: 'images/challenges' } + ); + setImage(url); + setImagePreview(null); + } catch (error) { + console.error('[CHALLENGE] 封面上传失败', error); + Alert.alert('上传失败', '封面上传失败,请稍后重试'); + } + } catch (error) { + console.error('[CHALLENGE] 拍照失败', error); + Alert.alert('拍照失败', '无法打开相机,请稍后再试'); + } + }, + }, + { + text: '从相册选择', + onPress: async () => { + try { + const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (permission.status !== 'granted') { + Alert.alert('权限不足', '需要相册权限以选择封面'); + return; + } + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + quality: 0.9, + }); + if (result.canceled || !result.assets?.length) return; + const asset = result.assets[0]; + setImagePreview(asset.uri); + setImage(undefined); + try { + const { url } = await upload( + { + uri: asset.uri, + name: asset.fileName ?? `challenge-${Date.now()}.jpg`, + type: asset.mimeType ?? 'image/jpeg', + }, + { prefix: 'images/challenges' } + ); + setImage(url); + setImagePreview(null); + } catch (error) { + console.error('[CHALLENGE] 封面上传失败', error); + Alert.alert('上传失败', '封面上传失败,请稍后重试'); + } + } catch (error) { + console.error('[CHALLENGE] 选择封面失败', error); + Alert.alert('选择失败', '无法打开相册,请稍后再试'); + } + }, + }, + { text: '取消', style: 'cancel' }, + ], + { cancelable: true } + ); + }, [upload]); + + const handleViewChallenge = () => { + setShareModalVisible(false); + if (createdChallengeId) { + router.replace({ pathname: '/challenges/[id]', params: { id: createdChallengeId } }); + } + }; + + const renderField = ( + label: string, + value: string, + onChange: (val: string) => void, + placeholder?: string, + keyboardType: 'default' | 'numeric' = 'default', + onFocus?: () => void + ) => ( + + {label} + + + ); + + const renderTextarea = ( + label: string, + value: string, + onChange: (val: string) => void, + placeholder?: string + ) => ( + + {label} + + + ); + + const progressMeta = `${durationDays} 天 · ${progressUnit || '天'}`; + const heroImageSource = imagePreview || image || FALLBACK_IMAGE; + + return ( + + + + + + + + + + 自定义挑战 + {title || '你的专属挑战'} + {progressMeta} + + + + + + 基础信息 + {createError ? {createError} : null} + + {renderField('标题', title, setTitle, '挑战标题(最多100字)')} + + 封面图 + + + {uploading ? '上传中…' : '上传封面'} + + {image || imagePreview ? ( + { + setImagePreview(null); + setImage(undefined); + }} + > + 清除 + + ) : null} + + 建议比例 16:9,清晰展示挑战氛围 + + {renderTextarea('挑战说明', summary, setSummary, '简单介绍这个挑战的目标与要求')} + + + + 挑战设置 + + + 挑战类型 + + {typeOptions.map((option) => { + const active = option.value === type; + return ( + setType(option.value)} + style={[ + styles.chip, + active && { backgroundColor: `${option.accent}1A`, borderColor: option.accent }, + ]} + > + + {option.label} + + + ); + })} + + + + + 时间范围 + + setPickerType('start')} + > + 开始 + {dayjs(startDate).format('YYYY.MM.DD')} + + setPickerType('end')} + > + 结束 + {dayjs(endDate).format('YYYY.MM.DD')} + + + + + + + 持续时间 + + {durationLabel} + + + {renderField('周期标签', periodLabel, (v) => { + setPeriodEdited(true); + setPeriodLabel(v); + }, '如:21天挑战')} + + + + {renderField('每日目标值', targetValue, handleTargetInputChange, '如:8', 'numeric')} + + 进度单位 + + {progressUnit} + + + + + {renderField('最少打卡天数', minimumCheckInDays, handleMinimumDaysChange, '至少1天', 'numeric')} + + {renderField('挑战要求说明', requirementLabel, setRequirementLabel, '例如:每日完成 30 分钟运动')} + + + + 展示&互动 + + {renderField('参与人数上限', maxParticipants, (v) => { + const digits = v.replace(/\D/g, ''); + if (!digits) { + setMaxParticipants(''); + return; + } + setMaxParticipants(String(parseInt(digits, 10))); + }, '留空表示无限制', 'numeric')} + + + + 是否公开 + 公开后其他用户可通过邀请码加入 + + + + + + + + + + + + 生成自定义挑战 + 自动创建分享码,邀请好友一起挑战 + + + + + {isCreating ? '创建中…' : '创建并生成邀请码'} + + + + + + + + setPickerType(null)} + /> + + setShareModalVisible(false)} + > + + + 邀请码已生成 + 分享给好友即可加入挑战 + + {shareCode ?? '获取中…'} + + + + 复制邀请码 + + + + 查看挑战 + + + + setShareModalVisible(false)} + > + 稍后再说 + + + + + + ); +} + +const styles = StyleSheet.create({ + screen: { + flex: 1, + }, + scrollContent: { + paddingBottom: 160, + }, + heroContainer: { + height: 260, + width: '100%', + overflow: 'hidden', + }, + heroImage: { + width: '100%', + height: '100%', + }, + heroOverlay: { + position: 'absolute', + bottom: 22, + left: 20, + right: 20, + }, + heroKicker: { + color: '#f8fafc', + fontSize: 13, + letterSpacing: 1.2, + fontWeight: '700', + }, + heroTitle: { + marginTop: 8, + fontSize: 26, + fontWeight: '800', + color: '#ffffff', + textShadowColor: 'rgba(0,0,0,0.25)', + textShadowOffset: { width: 0, height: 2 }, + textShadowRadius: 6, + }, + heroMeta: { + marginTop: 6, + fontSize: 14, + color: '#e2e8f0', + fontWeight: '600', + }, + formCard: { + marginTop: 14, + marginHorizontal: 20, + padding: 18, + borderRadius: 22, + backgroundColor: '#ffffff', + shadowColor: 'rgba(30, 41, 59, 0.12)', + shadowOpacity: 0.2, + shadowRadius: 20, + shadowOffset: { width: 0, height: 12 }, + elevation: 8, + gap: 10, + }, + sectionTitle: { + fontSize: 18, + fontWeight: '800', + color: '#0f172a', + }, + fieldBlock: { + gap: 6, + }, + fieldLabel: { + fontSize: 14, + fontWeight: '700', + color: '#0f172a', + }, + input: { + paddingHorizontal: 14, + paddingVertical: 12, + borderRadius: 14, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: '#f8fafc', + fontSize: 15, + color: '#111827', + }, + textarea: { + minHeight: 90, + }, + chipRow: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 10, + }, + chip: { + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 14, + backgroundColor: '#f8fafc', + borderWidth: 1, + borderColor: '#e5e7eb', + }, + chipLabel: { + fontSize: 13, + color: '#334155', + }, + uploadRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + uploadButton: { + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 12, + backgroundColor: '#EEF1FF', + borderWidth: 1, + borderColor: '#d1d5db', + }, + uploadButtonDisabled: { + opacity: 0.7, + }, + uploadButtonLabel: { + fontSize: 14, + fontWeight: '700', + color: '#4F5BD5', + }, + clearUpload: { + fontSize: 13, + fontWeight: '600', + color: '#9ca3af', + }, + helperText: { + marginTop: 6, + fontSize: 12, + color: '#6b7280', + }, + dateRow: { + flexDirection: 'row', + gap: 12, + }, + datePill: { + flex: 1, + padding: 12, + borderRadius: 14, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: '#f8fafc', + }, + dateLabel: { + fontSize: 12, + color: '#6b7280', + }, + dateValue: { + marginTop: 4, + fontSize: 15, + fontWeight: '700', + color: '#0f172a', + }, + readonlyPill: { + marginTop: 6, + paddingHorizontal: 14, + paddingVertical: 10, + borderRadius: 14, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: '#f8fafc', + }, + readonlyText: { + fontSize: 15, + fontWeight: '700', + color: '#0f172a', + }, + inlineFields: { + gap: 12, + }, + switchRow: { + marginTop: 6, + padding: 12, + borderRadius: 14, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: '#f8fafc', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + switchHint: { + marginTop: 4, + fontSize: 12, + color: '#6b7280', + }, + formHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + inlineError: { + fontSize: 12, + color: '#ef4444', + }, + floatingCTA: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + paddingHorizontal: 16, + paddingTop: 10, + }, + floatingBlur: { + borderRadius: 24, + overflow: 'hidden', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.6)', + backgroundColor: 'rgba(243, 244, 251, 0.9)', + }, + floatingContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + paddingHorizontal: 16, + paddingVertical: 14, + }, + floatingCopy: { + flex: 1, + }, + floatingTitle: { + fontSize: 15, + fontWeight: '800', + color: '#0f172a', + }, + floatingSubtitle: { + marginTop: 4, + fontSize: 12, + color: '#6b7280', + }, + floatingButton: { + borderRadius: 16, + overflow: 'hidden', + }, + floatingButtonBackground: { + paddingHorizontal: 18, + paddingVertical: 12, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + floatingButtonLabel: { + fontSize: 14, + fontWeight: '800', + color: '#ffffff', + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.35)', + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + shareCard: { + width: '100%', + padding: 20, + borderRadius: 22, + backgroundColor: '#ffffff', + shadowColor: 'rgba(15, 23, 42, 0.18)', + shadowOffset: { width: 0, height: 12 }, + shadowOpacity: 0.25, + shadowRadius: 20, + elevation: 12, + alignItems: 'center', + gap: 10, + }, + shareTitle: { + fontSize: 18, + fontWeight: '800', + color: '#0f172a', + }, + shareSubtitle: { + fontSize: 13, + color: '#6b7280', + }, + shareCodeBadge: { + marginTop: 10, + paddingHorizontal: 18, + paddingVertical: 12, + borderRadius: 16, + backgroundColor: '#EEF1FF', + }, + shareCode: { + fontSize: 22, + fontWeight: '800', + color: '#4F5BD5', + letterSpacing: 2, + }, + shareActions: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + gap: 12, + width: '100%', + marginTop: 8, + }, + shareButtonGhost: { + flex: 1, + paddingVertical: 12, + borderRadius: 14, + borderWidth: 1, + borderColor: '#d1d5db', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#f8fafc', + }, + shareButtonGhostLabel: { + fontSize: 14, + fontWeight: '700', + color: '#475569', + }, + shareButtonPrimary: { + flex: 1, + borderRadius: 14, + overflow: 'hidden', + }, + shareButtonPrimaryLabel: { + textAlign: 'center', + fontSize: 14, + fontWeight: '800', + color: '#ffffff', + paddingVertical: 12, + }, + shareClose: { + marginTop: 8, + paddingVertical: 10, + paddingHorizontal: 12, + }, + shareCloseLabel: { + fontSize: 13, + color: '#6b7280', + }, +}); diff --git a/app/index.tsx b/app/index.tsx index be993cb..9a4a1e0 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -2,7 +2,8 @@ import { ThemedView } from '@/components/ThemedView'; import { ROUTES } from '@/constants/Routes'; import { usePushNotifications } from '@/hooks/usePushNotifications'; import { useThemeColor } from '@/hooks/useThemeColor'; -import { preloadUserData } from '@/store/userSlice'; +import { STORAGE_KEYS } from '@/services/api'; +import AsyncStorage from '@/utils/kvStore'; import { router } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { ActivityIndicator, View } from 'react-native'; @@ -19,10 +20,11 @@ export default function SplashScreen() { const checkOnboardingStatus = async () => { try { - // 先预加载用户数据,包括 onboarding 状态 - console.log('开始预加载用户数据(包含 onboarding 状态)...'); - const userData = await preloadUserData(); - console.log('用户数据预加载完成,onboarding 状态:', userData.onboardingCompleted); + // 直接读取 onboarding 状态 + console.log('检查 onboarding 状态...'); + const onboardingCompletedStr = await AsyncStorage.getItem(STORAGE_KEYS.onboardingCompleted); + const onboardingCompleted = onboardingCompletedStr === 'true'; + console.log('Onboarding 状态:', onboardingCompleted); // 初始化推送通知(不阻塞应用启动,且不会请求权限) console.log('开始初始化推送通知基础服务...'); @@ -30,8 +32,8 @@ export default function SplashScreen() { console.warn('推送通知初始化失败,但不影响应用正常使用:', error); }); - // 根据预加载的状态决定跳转 - if (userData.onboardingCompleted) { + // 根据状态决定跳转 + if (onboardingCompleted) { console.log('用户已完成引导,跳转到统计页面'); router.replace(ROUTES.TAB_STATISTICS); } else { @@ -39,7 +41,7 @@ export default function SplashScreen() { router.replace(ROUTES.ONBOARDING); } } catch (error) { - console.error('检查引导状态或预加载用户数据失败:', error); + console.error('检查引导状态失败:', error); // 如果出现错误,默认进入主应用(假设已完成引导) router.replace(ROUTES.TAB_STATISTICS); } diff --git a/assets/fonts/ali-bold.ttf b/assets/fonts/ali-bold.ttf new file mode 100644 index 0000000..860a48a Binary files /dev/null and b/assets/fonts/ali-bold.ttf differ diff --git a/assets/fonts/ali-regular.ttf b/assets/fonts/ali-regular.ttf new file mode 100644 index 0000000..0a05b19 Binary files /dev/null and b/assets/fonts/ali-regular.ttf differ diff --git a/components/challenges/ChallengeProgressCard.tsx b/components/challenges/ChallengeProgressCard.tsx index 6a6623f..2e3861b 100644 --- a/components/challenges/ChallengeProgressCard.tsx +++ b/components/challenges/ChallengeProgressCard.tsx @@ -215,11 +215,13 @@ const styles = StyleSheet.create({ title: { fontSize: 18, fontWeight: '700', + fontFamily: 'AliBold' }, remaining: { fontSize: 11, fontWeight: '600', alignSelf: 'flex-start', + fontFamily: 'AliRegular' }, metaRow: { marginTop: 12, @@ -227,10 +229,12 @@ const styles = StyleSheet.create({ metaValue: { fontSize: 14, fontWeight: '700', + fontFamily: 'AliBold' }, metaSuffix: { fontSize: 13, fontWeight: '500', + fontFamily: 'AliBold' }, track: { marginTop: 12, diff --git a/components/ui/ConfirmationSheet.tsx b/components/ui/ConfirmationSheet.tsx index ae34582..d970644 100644 --- a/components/ui/ConfirmationSheet.tsx +++ b/components/ui/ConfirmationSheet.tsx @@ -1,10 +1,13 @@ +import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import * as Haptics from 'expo-haptics'; import React, { useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Animated, Dimensions, + KeyboardAvoidingView, Modal, + Platform, StyleSheet, Text, TouchableOpacity, @@ -24,6 +27,7 @@ interface ConfirmationSheetProps { cancelText?: string; destructive?: boolean; loading?: boolean; + content?: React.ReactNode; } export function ConfirmationSheet({ @@ -36,11 +40,13 @@ export function ConfirmationSheet({ cancelText = '取消', destructive = false, loading = false, + content, }: ConfirmationSheetProps) { const insets = useSafeAreaInsets(); const translateY = useRef(new Animated.Value(screenHeight)).current; const backdropOpacity = useRef(new Animated.Value(0)).current; const [modalVisible, setModalVisible] = useState(visible); + const isGlassAvailable = isLiquidGlassAvailable(); useEffect(() => { if (visible) { @@ -116,7 +122,10 @@ export function ConfirmationSheet({ onRequestClose={onClose} statusBarTranslucent > - + {title} {description ? {description} : null} + {content} - {cancelText} + {isGlassAvailable ? ( + + {cancelText} + + ) : ( + + {cancelText} + + )} - {loading ? ( - + {isGlassAvailable ? ( + + {loading ? ( + + ) : ( + {confirmText} + )} + ) : ( - {confirmText} + + {loading ? ( + + ) : ( + {confirmText} + )} + )} - + ); } @@ -221,8 +262,17 @@ const styles = StyleSheet.create({ gap: 12, marginTop: 8, }, - cancelButton: { + buttonContainer: { flex: 1, + }, + glassButton: { + height: 56, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + cancelButton: { height: 56, borderRadius: 18, borderWidth: 1, @@ -237,7 +287,6 @@ const styles = StyleSheet.create({ color: '#111827', }, confirmButton: { - flex: 1, height: 56, borderRadius: 18, alignItems: 'center', diff --git a/hooks/useAuthGuard.ts b/hooks/useAuthGuard.ts index 620e9b3..f8e603d 100644 --- a/hooks/useAuthGuard.ts +++ b/hooks/useAuthGuard.ts @@ -22,7 +22,9 @@ export function useAuthGuard() { const currentPath = usePathname(); const user = useAppSelector(state => state.user); - const isLoggedIn = !!user?.profile?.id; + // 判断登录状态:优先使用 token,因为 token 是登录的根本凭证 + // profile.id 可能在初始化时还未加载,但 token 已经从 AsyncStorage 恢复 + const isLoggedIn = !!user?.token; const ensureLoggedIn = useCallback(async (options?: EnsureOptions): Promise => { if (isLoggedIn) return true; diff --git a/i18n/index.ts b/i18n/index.ts index 295ccd6..2af1178 100644 --- a/i18n/index.ts +++ b/i18n/index.ts @@ -851,6 +851,222 @@ const medicationsResources = { }, }; +const challengeDetailResources = { + title: '挑战详情', + notFound: '未找到该挑战,稍后再试试吧。', + loading: '加载挑战详情中…', + retry: '重新加载', + share: { + generating: '正在生成分享卡片...', + failed: '分享失败,请稍后重试', + messageJoined: '我正在参与「{{title}}」挑战,已完成 {{completed}}/{{target}} 天!一起加入吧!', + messageNotJoined: '发现一个很棒的挑战「{{title}}」,一起来参与吧!', + }, + dateRange: { + format: '{{start}} - {{end}}', + monthDay: '{{month}}月{{day}}日', + ongoing: '持续更新中', + }, + participants: { + count: '{{count}} 人正在参与', + ongoing: '持续更新中', + more: '更多', + }, + detail: { + requirement: '按日打卡自动累计', + viewAllRanking: '查看全部', + }, + checkIn: { + title: '挑战打卡', + todayChecked: '今日已打卡', + subtitle: '每日打卡会累计进度,达成目标天数', + subtitleChecked: '已记录今日进度,明天继续保持', + button: { + checkIn: '立即打卡', + checking: '打卡中…', + checked: '今日已打卡', + notJoined: '加入后打卡', + upcoming: '挑战未开始', + expired: '挑战已结束', + }, + toast: { + alreadyChecked: '今日已打卡', + notStarted: '挑战未开始,开始后再来打卡', + expired: '挑战已结束,无法打卡', + mustJoin: '加入挑战后才能打卡', + success: '打卡成功,继续坚持!', + failed: '打卡失败,请稍后再试', + }, + }, + cta: { + join: '立即加入挑战', + joining: '加入中…', + leave: '退出挑战', + leaving: '退出中…', + upcoming: '挑战即将开始', + expired: '挑战已结束', + }, + highlight: { + join: { + title: '立即加入挑战', + subtitle: '邀请好友一起坚持,更容易收获成果', + }, + leave: { + title: '先别急着离开', + subtitle: '再坚持一下,下一个里程碑就要出现了', + }, + upcoming: { + title: '挑战即将开始', + subtitle: '{{date}} 开始,敬请期待', + subtitleFallback: '挑战即将开启,敬请期待', + }, + expired: { + title: '挑战已结束', + subtitle: '{{date}} 已截止,期待下一次挑战', + subtitleFallback: '本轮挑战已结束,期待下一次挑战', + }, + }, + alert: { + leaveConfirm: { + title: '确认退出挑战?', + message: '退出后需要重新加入才能继续坚持。', + cancel: '取消', + confirm: '退出挑战', + }, + joinFailed: '加入挑战失败', + leaveFailed: '退出挑战失败', + }, + ranking: { + title: '排行榜', + description: '', + empty: '榜单即将开启,快来抢占席位。', + }, + shareCard: { + footer: 'Out Live · 超越生命', + progress: { + label: '我的坚持进度', + days: '{{completed}} / {{target}} 天', + completed: '🎉 已完成挑战!', + remaining: '还差 {{remaining}} 天完成挑战', + }, + info: { + checkInDaily: '按日打卡', + joinUs: '快来一起坚持吧', + }, + shareCode: { + copied: '分享码已复制', + }, + }, +}; + +const challengeDetailResourcesEn = { + title: 'Challenge Details', + notFound: 'Challenge not found, please try again later.', + loading: 'Loading challenge details…', + retry: 'Reload', + share: { + generating: 'Generating share card...', + failed: 'Share failed, please try again later', + messageJoined: 'I\'m participating in "{{title}}" challenge, completed {{completed}}/{{target}} days! Join me!', + messageNotJoined: 'Found an amazing challenge "{{title}}", let\'s join together!', + }, + dateRange: { + format: '{{start}} - {{end}}', + monthDay: 'Month {{month}} Day {{day}}', + ongoing: 'Ongoing updates', + }, + participants: { + count: '{{count}} participants', + ongoing: 'Ongoing updates', + more: 'More', + }, + detail: { + requirement: 'Daily check-in auto accumulates', + viewAllRanking: 'View All', + }, + checkIn: { + title: 'Challenge Check-in', + todayChecked: 'Checked in today', + subtitle: 'Daily check-ins accumulate progress towards goal', + subtitleChecked: 'Today\'s progress recorded, keep it up tomorrow', + button: { + checkIn: 'Check In Now', + checking: 'Checking in…', + checked: 'Checked in today', + notJoined: 'Join to check in', + upcoming: 'Not started yet', + expired: 'Challenge ended', + }, + toast: { + alreadyChecked: 'Already checked in today', + notStarted: 'Challenge not started yet, check in after it begins', + expired: 'Challenge has ended, cannot check in', + mustJoin: 'Join the challenge to check in', + success: 'Check-in successful, keep going!', + failed: 'Check-in failed, please try again', + }, + }, + cta: { + join: 'Join Challenge', + joining: 'Joining…', + leave: 'Leave Challenge', + leaving: 'Leaving…', + upcoming: 'Starting Soon', + expired: 'Challenge Ended', + }, + highlight: { + join: { + title: 'Join Challenge Now', + subtitle: 'Invite friends to persist together, achieve more easily', + }, + leave: { + title: 'Don\'t leave just yet', + subtitle: 'Keep going, the next milestone is around the corner', + }, + upcoming: { + title: 'Challenge Starting Soon', + subtitle: 'Starts on {{date}}, stay tuned', + subtitleFallback: 'Challenge coming soon, stay tuned', + }, + expired: { + title: 'Challenge Ended', + subtitle: 'Ended on {{date}}, look forward to the next one', + subtitleFallback: 'This round has ended, look forward to the next challenge', + }, + }, + alert: { + leaveConfirm: { + title: 'Confirm leaving challenge?', + message: 'You will need to rejoin to continue.', + cancel: 'Cancel', + confirm: 'Leave Challenge', + }, + joinFailed: 'Failed to join challenge', + leaveFailed: 'Failed to leave challenge', + }, + ranking: { + title: 'Leaderboard', + description: '', + empty: 'Leaderboard opening soon, grab your spot.', + }, + shareCard: { + footer: 'Out Live · Beyond Life', + progress: { + label: 'My Progress', + days: '{{completed}} / {{target}} days', + completed: '🎉 Challenge Completed!', + remaining: '{{remaining}} days to complete', + }, + info: { + checkInDaily: 'Daily check-in', + joinUs: 'Join us!', + }, + shareCode: { + copied: 'Share code copied', + }, + }, +}; + const notificationSettingsResources = { title: '通知设置', loading: '加载中...', @@ -926,6 +1142,37 @@ const resources = { statistics: statisticsResources, medications: medicationsResources, notificationSettings: notificationSettingsResources, + challengeDetail: challengeDetailResources, + challenges: { + title: '挑战', + subtitle: '参与精选活动,保持每日动力', + loading: '加载挑战中…', + loadFailed: '加载挑战失败,请稍后重试', + retry: '重新加载', + empty: '暂无挑战,稍后再来探索。', + customChallenges: '自定义挑战', + officialChallenges: '暂无官方挑战,稍后再来探索。', + officialChallengesTitle: '官方挑战', + join: '加入', + create: '创建', + joined: '已加入', + invalidInviteCode: '请输入有效的邀请码', + joinSuccess: '加入挑战成功', + joinFailed: '加入失败,请稍后再试', + joinModal: { + title: '加入自定义挑战', + description: '输入 6-12 位邀请码,加入好友的挑战', + placeholder: '如:A3K9P2', + confirm: '确认加入', + cancel: '取消', + joining: '加入中…', + }, + statusLabels: { + upcoming: '即将开始', + ongoing: '进行中', + expired: '已结束', + }, + }, }, }, en: { @@ -1753,6 +2000,37 @@ const resources = { }, resetSuccess: 'Settings reset to default', }, + challengeDetail: challengeDetailResourcesEn, + challenges: { + title: 'Challenges', + subtitle: 'Join curated activities, stay motivated daily', + loading: 'Loading challenges…', + loadFailed: 'Failed to load challenges, please try again later', + retry: 'Retry', + empty: 'No challenges available, check back later.', + customChallenges: 'Custom Challenges', + officialChallenges: 'No official challenges available, check back later.', + officialChallengesTitle: 'Official Challenges', + join: 'Join', + create: 'Create', + joined: 'Joined', + invalidInviteCode: 'Please enter a valid invite code', + joinSuccess: 'Successfully joined challenge', + joinFailed: 'Failed to join challenge, please try again later', + joinModal: { + title: 'Join Custom Challenge', + description: 'Enter 6-12 digit invite code to join friend\'s challenge', + placeholder: 'e.g., A3K9P2', + confirm: 'Confirm Join', + cancel: 'Cancel', + joining: 'Joining…', + }, + statusLabels: { + upcoming: 'Upcoming', + ongoing: 'Ongoing', + expired: 'Expired', + }, + }, }, }, }; diff --git a/ios/OutLive.xcodeproj/project.pbxproj b/ios/OutLive.xcodeproj/project.pbxproj index bb8d61c..cc76989 100644 --- a/ios/OutLive.xcodeproj/project.pbxproj +++ b/ios/OutLive.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -91,7 +91,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -101,7 +101,18 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 79E80BA72EC5D92A004425BE /* medicine */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (79E80BBB2EC5D92B004425BE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = medicine; sourceTree = ""; }; + 79E80BA72EC5D92A004425BE /* medicine */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 79E80BBB2EC5D92B004425BE /* Exceptions for "medicine" folder in "medicineExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = medicine; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/ios/OutLive/Info.plist b/ios/OutLive/Info.plist index aa7fa3b..7455418 100644 --- a/ios/OutLive/Info.plist +++ b/ios/OutLive/Info.plist @@ -27,7 +27,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.30 + 1.1.1 CFBundleSignature ???? CFBundleURLTypes diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4ed097c..7846eff 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - React-Core - EXNotifications (0.32.12): - ExpoModulesCore - - Expo (54.0.21): + - Expo (54.0.25): - ExpoModulesCore - hermes-engine - RCTRequired @@ -35,25 +35,27 @@ PODS: - Yoga - ExpoAppleAuthentication (8.0.7): - ExpoModulesCore - - ExpoAsset (12.0.9): + - ExpoAsset (12.0.10): - ExpoModulesCore - ExpoBackgroundTask (1.0.8): - ExpoModulesCore - ExpoBlur (15.0.7): - ExpoModulesCore - - ExpoCamera (17.0.8): + - ExpoCamera (17.0.9): - ExpoModulesCore - ZXingObjC/OneD - ZXingObjC/PDF417 - - ExpoFileSystem (19.0.17): + - ExpoClipboard (8.0.7): + - ExpoModulesCore + - ExpoFileSystem (19.0.19): - ExpoModulesCore - ExpoFont (14.0.9): - ExpoModulesCore - - ExpoGlassEffect (0.1.5): + - ExpoGlassEffect (0.1.7): - ExpoModulesCore - ExpoHaptics (15.0.7): - ExpoModulesCore - - ExpoHead (6.0.14): + - ExpoHead (6.0.15): - ExpoModulesCore - RNScreens - ExpoImage (3.0.10): @@ -69,14 +71,14 @@ PODS: - ExpoModulesCore - ExpoLinearGradient (15.0.7): - ExpoModulesCore - - ExpoLinking (8.0.8): + - ExpoLinking (8.0.9): - ExpoModulesCore - ExpoLocalization (17.0.7): - ExpoModulesCore - ExpoMediaLibrary (18.2.0): - ExpoModulesCore - React-Core - - ExpoModulesCore (3.0.23): + - ExpoModulesCore (3.0.26): - hermes-engine - RCTRequired - RCTTypeSafety @@ -101,7 +103,7 @@ PODS: - Yoga - ExpoQuickActions (6.0.0): - ExpoModulesCore - - ExpoSplashScreen (31.0.10): + - ExpoSplashScreen (31.0.11): - ExpoModulesCore - ExpoSQLite (16.0.8): - ExpoModulesCore @@ -161,8 +163,8 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - PurchasesHybridCommon (17.10.0): - - RevenueCat (= 5.43.0) + - PurchasesHybridCommon (17.19.1): + - RevenueCat (= 5.48.0) - RCTDeprecation (0.81.5) - RCTRequired (0.81.5) - RCTTypeSafety (0.81.5): @@ -1909,7 +1911,7 @@ PODS: - React-utils (= 0.81.5) - ReactNativeDependencies - ReactNativeDependencies (0.81.5) - - RevenueCat (5.43.0) + - RevenueCat (5.48.0) - RNCAsyncStorage (2.2.0): - hermes-engine - RCTRequired @@ -1954,7 +1956,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNCPicker (2.11.1): + - RNCPicker (2.11.4): - hermes-engine - RCTRequired - RCTTypeSafety @@ -1976,7 +1978,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNDateTimePicker (8.4.4): + - RNDateTimePicker (8.5.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2022,10 +2024,10 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNPurchases (9.5.4): - - PurchasesHybridCommon (= 17.10.0) + - RNPurchases (9.6.7): + - PurchasesHybridCommon (= 17.19.1) - React-Core - - RNReanimated (4.1.3): + - RNReanimated (4.1.5): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2047,10 +2049,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated (= 4.1.3) + - RNReanimated/reanimated (= 4.1.5) - RNWorklets - Yoga - - RNReanimated/reanimated (4.1.3): + - RNReanimated/reanimated (4.1.5): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2072,10 +2074,10 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - RNReanimated/reanimated/apple (= 4.1.3) + - RNReanimated/reanimated/apple (= 4.1.5) - RNWorklets - Yoga - - RNReanimated/reanimated/apple (4.1.3): + - RNReanimated/reanimated/apple (4.1.5): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2146,7 +2148,7 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - RNSentry (7.2.0): + - RNSentry (7.7.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2168,7 +2170,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - ReactNativeDependencies - - Sentry/HybridSDK (= 8.56.1) + - Sentry/HybridSDK (= 8.57.3) - Yoga - RNSVG (15.12.1): - hermes-engine @@ -2297,7 +2299,7 @@ PODS: - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - - Sentry/HybridSDK (8.56.1) + - Sentry/HybridSDK (8.57.3) - UMAppLoader (6.0.7) - Yoga (0.0.0) - ZXingObjC/Core (3.6.9) @@ -2317,6 +2319,7 @@ DEPENDENCIES: - ExpoBackgroundTask (from `../node_modules/expo-background-task/ios`) - ExpoBlur (from `../node_modules/expo-blur/ios`) - ExpoCamera (from `../node_modules/expo-camera/ios`) + - ExpoClipboard (from `../node_modules/expo-clipboard/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) - ExpoGlassEffect (from `../node_modules/expo-glass-effect/ios`) @@ -2462,6 +2465,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-blur/ios" ExpoCamera: :path: "../node_modules/expo-camera/ios" + ExpoClipboard: + :path: "../node_modules/expo-clipboard/ios" ExpoFileSystem: :path: "../node_modules/expo-file-system/ios" ExpoFont: @@ -2683,27 +2688,28 @@ SPEC CHECKSUMS: EXConstants: fd688cef4e401dcf798a021cfb5d87c890c30ba3 EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05 EXNotifications: 7cff475adb5d7a255a9ea46bbd2589cb3b454506 - Expo: 27ae59be9be4feab2b1c1ae06550752c524ca558 + Expo: 111394d38f32be09385d4c7f70cc96d2da438d0d ExpoAppleAuthentication: bc9de6e9ff3340604213ab9031d4c4f7f802623e - ExpoAsset: 9ba6fbd677fb8e241a3899ac00fa735bc911eadf + ExpoAsset: d839c8eae8124470332408427327e8f88beb2dfd ExpoBackgroundTask: e0d201d38539c571efc5f9cb661fae8ab36ed61b ExpoBlur: 2dd8f64aa31f5d405652c21d3deb2d2588b1852f - ExpoCamera: e75f6807a2c047f3338bbadd101af4c71a1d13a5 - ExpoFileSystem: b79eadbda7b7f285f378f95f959cc9313a1c9c61 + ExpoCamera: 2a87c210f8955350ea5c70f1d539520b2fc5d940 + ExpoClipboard: af650d14765f19c60ce2a1eaf9dfe6445eff7365 + ExpoFileSystem: 77157a101e03150a4ea4f854b4dd44883c93ae0a ExpoFont: cf9d90ec1d3b97c4f513211905724c8171f82961 - ExpoGlassEffect: 779c46bd04ea47ba4726efb73267b5bcc6abd664 + ExpoGlassEffect: 265fa3d75b46bc58262e4dfa513135fa9dfe4aac ExpoHaptics: 807476b0c39e9d82b7270349d6487928ce32df84 - ExpoHead: e317214fa14edeaf17748d39ec9e550a3d1194fb + ExpoHead: 95a6ee0be1142320bccf07961d6a1502ded5d6ac ExpoImage: 9c3428921c536ab29e5c6721d001ad5c1f469566 ExpoImagePicker: d251aab45a1b1857e4156fed88511b278b4eee1c ExpoKeepAwake: 1a2e820692e933c94a565ec3fbbe38ac31658ffe ExpoLinearGradient: a464898cb95153125e3b81894fd479bcb1c7dd27 - ExpoLinking: f051f28e50ea9269ff539317c166adec81d9342d + ExpoLinking: 77455aa013e9b6a3601de03ecfab09858ee1b031 ExpoLocalization: b852a5d8ec14c5349c1593eca87896b5b3ebfcca ExpoMediaLibrary: 641a6952299b395159ccd459bd8f5f6764bf55fe - ExpoModulesCore: 5f20603cf25698682d7c43c05fbba8c748b189d2 + ExpoModulesCore: e8ec7f8727caf51a49d495598303dd420ca994bf ExpoQuickActions: 31a70aa6a606128de4416a4830e09cfabfe6667f - ExpoSplashScreen: cbb839de72110dea1851dd3e85080b7923af2540 + ExpoSplashScreen: 268b2f128dc04284c21010540a6c4dd9f95003e3 ExpoSQLite: 7fa091ba5562474093fef09be644161a65e11b3f ExpoSymbols: 1ae04ce686de719b9720453b988d8bc5bf776c68 ExpoSystemUI: 2761aa6875849af83286364811d46e8ed8ea64c7 @@ -2717,7 +2723,7 @@ SPEC CHECKSUMS: libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c - PurchasesHybridCommon: b7b4eafb55fbaaac19b4c36d4082657a3f0d8490 + PurchasesHybridCommon: a4837eebc889b973668af685d6c23b89a038461d RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990 RCTRequired: 8f3cfc90cc25cf6e420ddb3e7caaaabc57df6043 RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c @@ -2787,24 +2793,24 @@ SPEC CHECKSUMS: ReactCodegen: 7d4593f7591f002d137fe40cef3f6c11f13c88cc ReactCommon: 08810150b1206cc44aecf5f6ae19af32f29151a8 ReactNativeDependencies: 71ce9c28beb282aa720ea7b46980fff9669f428a - RevenueCat: a51003d4cb33820cc504cf177c627832b462a98e + RevenueCat: 1e61140a343a77dc286f171b3ffab99ca09a4b57 RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4 RNCMaskedView: d2578d41c59b936db122b2798ba37e4722d21035 - RNCPicker: a7170edbcbf8288de8edb2502e08e7fc757fa755 - RNDateTimePicker: be0e44bcb9ed0607c7c5f47dbedd88cf091f6791 + RNCPicker: c8a3584b74133464ee926224463fcc54dfdaebca + RNDateTimePicker: 19ffa303c4524ec0a2dfdee2658198451c16b7f1 RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c RNGestureHandler: 2914750df066d89bf9d8f48a10ad5f0051108ac3 - RNPurchases: 2569675abdc1dbc739f2eec0fa564a112cf860de - RNReanimated: 3895a29fdf77bbe2a627e1ed599a5e5d1df76c29 + RNPurchases: 5f3cd4fea5ef2b3914c925b2201dd5cecd31922f + RNReanimated: 1442a577e066e662f0ce1cd1864a65c8e547aee0 RNScreens: d8d6f1792f6e7ac12b0190d33d8d390efc0c1845 - RNSentry: 41979b419908128847ef662cc130a400b7576fa9 + RNSentry: 1d7b9fdae7a01ad8f9053335b5d44e75c39a955e RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34 RNWorklets: 54d8dffb7f645873a58484658ddfd4bd1a9a0bc1 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - Sentry: b3ec44d01708fce73f99b544beb57e890eca4406 + Sentry: c643eb180df401dd8c734c5036ddd9dd9218daa6 UMAppLoader: e1234c45d2b7da239e9e90fc4bbeacee12afd5b6 Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 diff --git a/package-lock.json b/package-lock.json index 0540456..bb967ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,27 +10,28 @@ "dependencies": { "@expo/metro-runtime": "~6.1.2", "@expo/ui": "~0.2.0-beta.7", - "@expo/vector-icons": "^15.0.2", + "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "^2.2.0", - "@react-native-community/datetimepicker": "8.4.4", + "@react-native-community/datetimepicker": "8.5.1", "@react-native-masked-view/masked-view": "^0.3.2", - "@react-native-picker/picker": "2.11.1", + "@react-native-picker/picker": "2.11.4", "@react-native-voice/voice": "^3.2.4", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/elements": "^2.6.4", - "@react-navigation/native": "^7.1.8", - "@reduxjs/toolkit": "^2.9.0", - "@sentry/react-native": "~7.2.0", - "@types/lodash": "^4.17.20", - "dayjs": "^1.11.18", - "expo": "54.0.21", + "@react-navigation/bottom-tabs": "^7.8.6", + "@react-navigation/elements": "^2.8.3", + "@react-navigation/native": "^7.1.21", + "@reduxjs/toolkit": "^2.11.0", + "@sentry/react-native": "~7.7.0", + "@types/lodash": "^4.17.21", + "dayjs": "^1.11.19", + "expo": "54.0.25", "expo-apple-authentication": "~8.0.7", "expo-background-task": "~1.0.8", "expo-blur": "~15.0.7", - "expo-camera": "~17.0.8", - "expo-constants": "~18.0.9", + "expo-camera": "~17.0.9", + "expo-clipboard": "~8.0.7", + "expo-constants": "~18.0.10", "expo-font": "~14.0.9", - "expo-glass-effect": "~0.1.5", + "expo-glass-effect": "~0.1.7", "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", "expo-image-picker": "~17.0.8", @@ -40,8 +41,8 @@ "expo-media-library": "^18.2.0", "expo-notifications": "~0.32.12", "expo-quick-actions": "^6.0.0", - "expo-router": "~6.0.14", - "expo-splash-screen": "~31.0.10", + "expo-router": "~6.0.15", + "expo-splash-screen": "~31.0.11", "expo-sqlite": "^16.0.8", "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", @@ -168,7 +169,7 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { @@ -195,17 +196,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", + "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", + "@babel/traverse": "^7.28.5", "semver": "^6.3.1" }, "engines": { @@ -216,13 +217,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -258,13 +259,13 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -302,7 +303,7 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "license": "MIT", "dependencies": { @@ -340,7 +341,7 @@ }, "node_modules/@babel/helper-replace-supers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "license": "MIT", "dependencies": { @@ -357,7 +358,7 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "license": "MIT", "dependencies": { @@ -527,6 +528,7 @@ "version": "7.28.0", "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", @@ -703,7 +705,7 @@ }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "license": "MIT", "dependencies": { @@ -820,7 +822,7 @@ }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "license": "MIT", "dependencies": { @@ -835,7 +837,7 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "license": "MIT", "dependencies": { @@ -899,7 +901,7 @@ }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "license": "MIT", "dependencies": { @@ -930,9 +932,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", - "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", + "version": "7.28.4", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -940,7 +942,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1077,7 +1079,7 @@ }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "license": "MIT", "dependencies": { @@ -1109,7 +1111,7 @@ }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "license": "MIT", "dependencies": { @@ -1172,9 +1174,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1367,7 +1369,7 @@ }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "license": "MIT", "dependencies": { @@ -1428,13 +1430,13 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", + "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.28.5", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" @@ -1448,7 +1450,7 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "license": "MIT", "dependencies": { @@ -1483,16 +1485,16 @@ } }, "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "version": "7.28.5", + "resolved": "https://mirrors.tencent.com/npm/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" + "@babel/plugin-transform-typescript": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1876,9 +1878,9 @@ } }, "node_modules/@expo/fingerprint": { - "version": "0.15.2", - "resolved": "https://mirrors.tencent.com/npm/@expo/fingerprint/-/fingerprint-0.15.2.tgz", - "integrity": "sha512-mA3weHEOd9B3mbDLNDKmAcFWo3kqsAJqPne7uMJndheKXPbRw15bV+ajAGBYZh2SS37xixLJ5eDpuc+Wr6jJtw==", + "version": "0.15.3", + "resolved": "https://mirrors.tencent.com/npm/@expo/fingerprint/-/fingerprint-0.15.3.tgz", + "integrity": "sha512-8YPJpEYlmV171fi+t+cSLMX1nC5ngY9j2FiN70dHldLpd6Ct6ouGhk96svJ4BQZwsqwII2pokwzrDAwqo4Z0FQ==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -1924,6 +1926,7 @@ "version": "7.7.3", "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -1981,9 +1984,9 @@ } }, "node_modules/@expo/mcp-tunnel": { - "version": "0.0.8", - "resolved": "https://mirrors.tencent.com/npm/@expo/mcp-tunnel/-/mcp-tunnel-0.0.8.tgz", - "integrity": "sha512-6261obzt6h9TQb6clET7Fw4Ig4AY2hfTNKI3gBt0gcTNxZipwMg8wER7ssDYieA9feD/FfPTuCPYFcR280aaWA==", + "version": "0.1.0", + "resolved": "https://mirrors.tencent.com/npm/@expo/mcp-tunnel/-/mcp-tunnel-0.1.0.tgz", + "integrity": "sha512-rJ6hl0GnIZj9+ssaJvFsC7fwyrmndcGz+RGFzu+0gnlm78X01957yjtHgjcmnQAgL5hWEOR6pkT0ijY5nU5AWw==", "license": "MIT", "dependencies": { "ws": "^8.18.3", @@ -2041,9 +2044,9 @@ } }, "node_modules/@expo/metro-config": { - "version": "54.0.8", - "resolved": "https://mirrors.tencent.com/npm/@expo/metro-config/-/metro-config-54.0.8.tgz", - "integrity": "sha512-rCkDQ8IT6sgcGNy48O2cTE4NlazCAgAIsD5qBsNPJLZSS0XbaILvAgGsFt/4nrx0GMGj6iQcOn5ifwV4NssTmw==", + "version": "54.0.9", + "resolved": "https://mirrors.tencent.com/npm/@expo/metro-config/-/metro-config-54.0.9.tgz", + "integrity": "sha512-CRI4WgFXrQ2Owyr8q0liEBJveUIF9DcYAKadMRsJV7NxGNBdrIIKzKvqreDfsGiRqivbLsw6UoNb3UE7/SvPfg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.20.0", @@ -2576,6 +2579,7 @@ "version": "7.10.4", "resolved": "https://mirrors.tencent.com/npm/@babel/code-frame/-/code-frame-7.10.4.tgz", "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", "dependencies": { "@babel/highlight": "^7.10.4" } @@ -3333,9 +3337,9 @@ } }, "node_modules/@react-native-community/datetimepicker": { - "version": "8.4.4", - "resolved": "https://mirrors.tencent.com/npm/@react-native-community/datetimepicker/-/datetimepicker-8.4.4.tgz", - "integrity": "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==", + "version": "8.5.1", + "resolved": "https://mirrors.tencent.com/npm/@react-native-community/datetimepicker/-/datetimepicker-8.5.1.tgz", + "integrity": "sha512-TuwM1ORbxCjOp1GOtONj0QnpDpVfq0F4UlfKZYPxL/vmriaLHt2Kgvw63Bv0Bpep4eOkslVVSS1IRfRI6d392g==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -3366,9 +3370,9 @@ } }, "node_modules/@react-native-picker/picker": { - "version": "2.11.1", - "resolved": "https://mirrors.tencent.com/npm/@react-native-picker/picker/-/picker-2.11.1.tgz", - "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "version": "2.11.4", + "resolved": "https://mirrors.tencent.com/npm/@react-native-picker/picker/-/picker-2.11.4.tgz", + "integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==", "license": "MIT", "workspaces": [ "example" @@ -3787,16 +3791,17 @@ } }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.4.8", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.8.tgz", - "integrity": "sha512-W85T9f5sPA2zNnkxBO0PF0Jg9CRAMYqD9hY20dAhuVM5I+qiCqhW7qLveK59mlbtdXuGmieit6FK3inKmXzL7A==", + "version": "7.8.6", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/bottom-tabs/-/bottom-tabs-7.8.6.tgz", + "integrity": "sha512-0wGtU+I1rCUjvAqKtzD2dwQaTICFf5J233vkg20cLrx8LNQPAgSsbnsDSM6S315OOoVLCIL1dcrNv7ExLBlWfw==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.6.5", - "color": "^4.2.3" + "@react-navigation/elements": "^2.8.3", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" }, "peerDependencies": { - "@react-navigation/native": "^7.1.18", + "@react-navigation/native": "^7.1.21", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3804,13 +3809,14 @@ } }, "node_modules/@react-navigation/core": { - "version": "7.12.4", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/core/-/core-7.12.4.tgz", - "integrity": "sha512-xLFho76FA7v500XID5z/8YfGTvjQPw7/fXsq4BIrVSqetNe/o/v+KAocEw4ots6kyv3XvSTyiWKh2g3pN6xZ9Q==", + "version": "7.13.2", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/core/-/core-7.13.2.tgz", + "integrity": "sha512-A0pFeZlKp+FJob2lVr7otDt3M4rsSJrnAfXWoWR9JVeFtfEXsH/C0s7xtpDCMRUO58kzSBoTF1GYzoMC5DLD4g==", "license": "MIT", "dependencies": { - "@react-navigation/routers": "^7.5.1", + "@react-navigation/routers": "^7.5.2", "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", @@ -3822,9 +3828,9 @@ } }, "node_modules/@react-navigation/elements": { - "version": "2.6.5", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/elements/-/elements-2.6.5.tgz", - "integrity": "sha512-HOaekvFeoqKyaSKP2hakL7OUnw0jIhk/1wMjcovUKblT76LMTumZpriqsc30m/Vnyy1a8zgp4VsuA1xftcalgQ==", + "version": "2.8.3", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/elements/-/elements-2.8.3.tgz", + "integrity": "sha512-0c5nSDPP3bUFujgkSVqqMShaAup3XIxNe1KTK9LSmwKgWEneyo6OPIjIdiEwPlZvJZKi7ag5hDjacQLGwO0LGA==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -3833,7 +3839,7 @@ }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", - "@react-navigation/native": "^7.1.18", + "@react-navigation/native": "^7.1.21", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" @@ -3845,12 +3851,12 @@ } }, "node_modules/@react-navigation/native": { - "version": "7.1.18", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/native/-/native-7.1.18.tgz", - "integrity": "sha512-DZgd6860dxcq3YX7UzIXeBr6m3UgXvo9acxp5jiJyIZXdR00Br9JwVkO7e0bUeTA2d3Z8dsmtAR84Y86NnH64Q==", + "version": "7.1.21", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/native/-/native-7.1.21.tgz", + "integrity": "sha512-mhpAewdivBL01ibErr91FUW9bvKhfAF6Xv/yr6UOJtDhv0jU6iUASUcA3i3T8VJCOB/vxmoke7VDp8M+wBFs/Q==", "license": "MIT", "dependencies": { - "@react-navigation/core": "^7.12.4", + "@react-navigation/core": "^7.13.2", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", @@ -3862,16 +3868,18 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.3.27", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/native-stack/-/native-stack-7.3.27.tgz", - "integrity": "sha512-bbbud0pT63tGh706hQD/A3Z9gF1O2HtQ0dJqaiYzHzPy9wSOi82i721530tJkmccevAemUrZbEeEC5mxVo1DzQ==", + "version": "7.8.0", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/native-stack/-/native-stack-7.8.0.tgz", + "integrity": "sha512-iRqQY+IYB610BJY/335/kdNDhXQ8L9nPUlIT+DSk88FA86+C+4/vek8wcKw8IrfwdorT4m+6TY0v7Qnrt+WLKQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.6.5", + "@react-navigation/elements": "^2.8.3", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", "warn-once": "^0.1.1" }, "peerDependencies": { - "@react-navigation/native": "^7.1.18", + "@react-navigation/native": "^7.1.21", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0", @@ -3879,23 +3887,23 @@ } }, "node_modules/@react-navigation/routers": { - "version": "7.5.1", - "resolved": "https://mirrors.tencent.com/npm/@react-navigation/routers/-/routers-7.5.1.tgz", - "integrity": "sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==", + "version": "7.5.2", + "resolved": "https://mirrors.tencent.com/npm/@react-navigation/routers/-/routers-7.5.2.tgz", + "integrity": "sha512-kymreY5aeTz843E+iPAukrsOtc7nabAH6novtAPREmmGu77dQpfxPB2ZWpKb5nRErIRowp1kYRoN2Ckl+S6JYw==", "license": "MIT", "dependencies": { "nanoid": "^3.3.11" } }, "node_modules/@reduxjs/toolkit": { - "version": "2.9.0", - "resolved": "https://mirrors.tencent.com/npm/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", - "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "version": "2.11.0", + "resolved": "https://mirrors.tencent.com/npm/@reduxjs/toolkit/-/toolkit-2.11.0.tgz", + "integrity": "sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", + "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" @@ -3914,24 +3922,24 @@ } }, "node_modules/@revenuecat/purchases-js": { - "version": "1.15.0", - "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js/-/purchases-js-1.15.0.tgz", - "integrity": "sha512-bBzDwTvrwkpW9vFmK7u5tWf2aT1PAHbK7KMr9cXkMpaZy8ct39IP/4hIQLWPAEQBlTc7GvoRhgnDpwLMwJTDpg==", + "version": "1.18.3", + "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js/-/purchases-js-1.18.3.tgz", + "integrity": "sha512-n43M5aRqC3dnWizJPx930cJSZ9M3UxiTiFri/4msH4wmqIb30nRspDdoEt79cGybUxcSt85Ma/Ot7AYmn/IClg==", "license": "MIT" }, "node_modules/@revenuecat/purchases-js-hybrid-mappings": { - "version": "17.10.0", - "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-17.10.0.tgz", - "integrity": "sha512-oSofBs7oGxX2N9/CyWpAcvlXvUe8VdS5J2J4SWHWCeCohgkOI4xsAPrbisXYPiNaILLFt/slD1mqL3sawzLBug==", + "version": "17.19.1", + "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-js-hybrid-mappings/-/purchases-js-hybrid-mappings-17.19.1.tgz", + "integrity": "sha512-fwJvk/Tyw2t9P58AkJNDeVo41Ihh+l61ZhunWaAqSEIlJysGojWJOQrCvOAIXqGDwUObbntZ4BRNJ3VjOFuRrQ==", "license": "MIT", "dependencies": { - "@revenuecat/purchases-js": "1.15.0" + "@revenuecat/purchases-js": "1.18.3" } }, "node_modules/@revenuecat/purchases-typescript-internal": { - "version": "17.10.0", - "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-17.10.0.tgz", - "integrity": "sha512-ILcAS3ixsij/06sy5ZtA62R3oscYVB+0fGCMmfgta3zrY5gM5dBto5YzSerSL57icSJhz2RbHYlGsUknQzJ1pQ==", + "version": "17.19.1", + "resolved": "https://mirrors.tencent.com/npm/@revenuecat/purchases-typescript-internal/-/purchases-typescript-internal-17.19.1.tgz", + "integrity": "sha512-OGhDqdUQTGc8Vfhv0DERo7E+WdaiiNr157gFYnoeaTjh/eCBD3GoDzRP8HR5QBnrBWhVgT0KpRAZnzEnBwhjDg==", "license": "MIT" }, "node_modules/@rtsao/scc": { @@ -3942,84 +3950,84 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/browser-utils/-/browser-utils-10.12.0.tgz", - "integrity": "sha512-dozbx389jhKynj0d657FsgbBVOar7pX3mb6GjqCxslXF0VKpZH2Xks0U32RgDY/nK27O+o095IWz7YvjVmPkDw==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/browser-utils/-/browser-utils-10.26.0.tgz", + "integrity": "sha512-rPg1+JZlfp912pZONQAWZzbSaZ9L6R2VrMcCEa+2e2Gqk9um4b+LqF5RQWZsbt5Z0n0azSy/KQ6zAe/zTPXSOg==", "license": "MIT", "dependencies": { - "@sentry/core": "10.12.0" + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/feedback/-/feedback-10.12.0.tgz", - "integrity": "sha512-0+7ceO6yQPPqfxRc9ue/xoPHKcnB917ezPaehGQNfAFNQB9PNTG1y55+8mRu0Fw+ANbZeCt/HyoCmXuRdxmkpg==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/feedback/-/feedback-10.26.0.tgz", + "integrity": "sha512-0vk9eQP0CXD7Y2WkcCIWHaAqnXOAi18/GupgWLnbB2kuQVYVtStWxtW+OWRe8W/XwSnZ5m6JBTVeokuk/O16DQ==", "license": "MIT", "dependencies": { - "@sentry/core": "10.12.0" + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay/-/replay-10.12.0.tgz", - "integrity": "sha512-/1093gSNGN5KlOBsuyAl33JkzGiG38kCnxswQLZWpPpR6LBbR1Ddb18HjhDpoQNNEZybJBgJC3a5NKl43C2TSQ==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay/-/replay-10.26.0.tgz", + "integrity": "sha512-FMySQnY2/p0dVtFUBgUO+aMdK2ovqnd7Q/AkvMQUsN/5ulyj6KZx3JX3CqOqRtAr1izoCe4Kh2pi5t//sQmvsg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.12.0", - "@sentry/core": "10.12.0" + "@sentry-internal/browser-utils": "10.26.0", + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay-canvas/-/replay-canvas-10.12.0.tgz", - "integrity": "sha512-W/z1/+69i3INNfPjD1KuinSNaRQaApjzwb37IFmiyF440F93hxmEYgXHk3poOlYYaigl2JMYbysGPWOiXnqUXA==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry-internal/replay-canvas/-/replay-canvas-10.26.0.tgz", + "integrity": "sha512-vs7d/P+8M1L1JVAhhJx2wo15QDhqAipnEQvuRZ6PV7LUcS1un9/Vx49FMxpIkx6JcKADJVwtXrS1sX2hoNT/kw==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.12.0", - "@sentry/core": "10.12.0" + "@sentry-internal/replay": "10.26.0", + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "4.3.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz", - "integrity": "sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==", + "version": "4.6.1", + "resolved": "https://mirrors.tencent.com/npm/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.6.1.tgz", + "integrity": "sha512-aSIk0vgBqv7PhX6/Eov+vlI4puCE0bRXzUG5HdCsHBpAfeMkI8Hva6kSOusnzKqs8bf04hU7s3Sf0XxGTj/1AA==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/@sentry/browser": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/browser/-/browser-10.12.0.tgz", - "integrity": "sha512-lKyaB2NFmr7SxPjmMTLLhQ7xfxaY3kdkMhpzuRI5qwOngtKt4+FtvNYHRuz+PTtEFv4OaHhNNbRn6r91gWguQg==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry/browser/-/browser-10.26.0.tgz", + "integrity": "sha512-uvV4hnkt8bh8yP0disJ0fszy8FdnkyGtzyIVKdeQZbNUefwbDhd3H0KJrAHhJ5ocULMH3B+dipdPmw2QXbEflg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.12.0", - "@sentry-internal/feedback": "10.12.0", - "@sentry-internal/replay": "10.12.0", - "@sentry-internal/replay-canvas": "10.12.0", - "@sentry/core": "10.12.0" + "@sentry-internal/browser-utils": "10.26.0", + "@sentry-internal/feedback": "10.26.0", + "@sentry-internal/replay": "10.26.0", + "@sentry-internal/replay-canvas": "10.26.0", + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/cli": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli/-/cli-2.55.0.tgz", - "integrity": "sha512-cynvcIM2xL8ddwELyFRSpZQw4UtFZzoM2rId2l9vg7+wDREPDocMJB9lEQpBIo3eqhp9JswqUT037yjO6iJ5Sw==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli/-/cli-2.58.2.tgz", + "integrity": "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4036,20 +4044,20 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.55.0", - "@sentry/cli-linux-arm": "2.55.0", - "@sentry/cli-linux-arm64": "2.55.0", - "@sentry/cli-linux-i686": "2.55.0", - "@sentry/cli-linux-x64": "2.55.0", - "@sentry/cli-win32-arm64": "2.55.0", - "@sentry/cli-win32-i686": "2.55.0", - "@sentry/cli-win32-x64": "2.55.0" + "@sentry/cli-darwin": "2.58.2", + "@sentry/cli-linux-arm": "2.58.2", + "@sentry/cli-linux-arm64": "2.58.2", + "@sentry/cli-linux-i686": "2.58.2", + "@sentry/cli-linux-x64": "2.58.2", + "@sentry/cli-win32-arm64": "2.58.2", + "@sentry/cli-win32-i686": "2.58.2", + "@sentry/cli-win32-x64": "2.58.2" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-darwin/-/cli-darwin-2.55.0.tgz", - "integrity": "sha512-jGHE7SHHzqXUmnsmRLgorVH6nmMmTjQQXdPZbSL5tRtH8d3OIYrVNr5D72DSgD26XAPBDMV0ibqOQ9NKoiSpfA==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-darwin/-/cli-darwin-2.58.2.tgz", + "integrity": "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w==", "license": "BSD-3-Clause", "optional": true, "os": [ @@ -4060,9 +4068,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm/-/cli-linux-arm-2.55.0.tgz", - "integrity": "sha512-ATjU0PsiWADSPLF/kZroLZ7FPKd5W9TDWHVkKNwIUNTei702LFgTjNeRwOIzTgSvG3yTmVEqtwFQfFN/7hnVXQ==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.2.tgz", + "integrity": "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg==", "cpu": [ "arm" ], @@ -4078,9 +4086,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.55.0.tgz", - "integrity": "sha512-jNB/0/gFcOuDCaY/TqeuEpsy/k52dwyk1SOV3s1ku4DUsln6govTppeAGRewY3T1Rj9B2vgIWTrnB8KVh9+Rgg==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.2.tgz", + "integrity": "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g==", "cpu": [ "arm64" ], @@ -4096,9 +4104,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-i686/-/cli-linux-i686-2.55.0.tgz", - "integrity": "sha512-8LZjo6PncTM6bWdaggscNOi5r7F/fqRREsCwvd51dcjGj7Kp1plqo9feEzYQ+jq+KUzVCiWfHrUjddFmYyZJrg==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.2.tgz", + "integrity": "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA==", "cpu": [ "x86", "ia32" @@ -4115,9 +4123,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-x64/-/cli-linux-x64-2.55.0.tgz", - "integrity": "sha512-5LUVvq74Yj2cZZy5g5o/54dcWEaX4rf3myTHy73AKhRj1PABtOkfexOLbF9xSrZy95WXWaXyeH+k5n5z/vtHfA==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.2.tgz", + "integrity": "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A==", "cpu": [ "x64" ], @@ -4133,9 +4141,9 @@ } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.55.0.tgz", - "integrity": "sha512-cWIQdzm1pfLwPARsV6dUb8TVd6Y3V1A2VWxjTons3Ift6GvtVmiAe0OWL8t2Yt95i8v61kTD/6Tq21OAaogqzA==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.2.tgz", + "integrity": "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w==", "cpu": [ "arm64" ], @@ -4149,9 +4157,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-i686/-/cli-win32-i686-2.55.0.tgz", - "integrity": "sha512-ldepCn2t9r4I0wvgk7NRaA7coJyy4rTQAzM66u9j5nTEsUldf66xym6esd5ZZRAaJUjffqvHqUIr/lrieTIrVg==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.2.tgz", + "integrity": "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q==", "cpu": [ "x86", "ia32" @@ -4166,9 +4174,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.55.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-x64/-/cli-win32-x64-2.55.0.tgz", - "integrity": "sha512-4hPc/I/9tXx+HLTdTGwlagtAfDSIa2AoTUP30tl32NAYQhx9a6niUbPAemK2qfxesiufJ7D2djX83rCw6WnJVA==", + "version": "2.58.2", + "resolved": "https://mirrors.tencent.com/npm/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.2.tgz", + "integrity": "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA==", "cpu": [ "x64" ], @@ -4207,22 +4215,22 @@ } }, "node_modules/@sentry/core": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/core/-/core-10.12.0.tgz", - "integrity": "sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry/core/-/core-10.26.0.tgz", + "integrity": "sha512-TjDe5QI37SLuV0q3nMOH8JcPZhv2e85FALaQMIhRILH9Ce6G7xW5GSjmH91NUVq8yc3XtiqYlz/EenEZActc4Q==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/react/-/react-10.12.0.tgz", - "integrity": "sha512-TpqgdoYbkf5JynmmW2oQhHQ/h5w+XPYk0cEb/UrsGlvJvnBSR+5tgh0AqxCSi3gvtp82rAXI5w1TyRPBbhLDBw==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry/react/-/react-10.26.0.tgz", + "integrity": "sha512-Qi0/FVXAalwQNr8zp0tocViH3+MRelW8ePqj3TdMzapkbXRuh07czdGgw8Zgobqcb7l4rRCRAUo2sl/H3KVkIw==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.12.0", - "@sentry/core": "10.12.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -4233,17 +4241,17 @@ } }, "node_modules/@sentry/react-native": { - "version": "7.2.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/react-native/-/react-native-7.2.0.tgz", - "integrity": "sha512-rjqYgEjntPz1sPysud78wi4B9ui7LBVPsG6qr8s/htLMYho9GPGFA5dF+eqsQWqMX8NDReAxNkLTC4+gCNklLQ==", + "version": "7.7.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry/react-native/-/react-native-7.7.0.tgz", + "integrity": "sha512-D+gqiw88mOnouY+Pd8A3wcUDOilPOIcypBPw7WL9v+K1jM12Snf6sosEG4xgFFMXoK+GSsYAeC5MR0skD/b+Zg==", "license": "MIT", "dependencies": { - "@sentry/babel-plugin-component-annotate": "4.3.0", - "@sentry/browser": "10.12.0", - "@sentry/cli": "2.55.0", - "@sentry/core": "10.12.0", - "@sentry/react": "10.12.0", - "@sentry/types": "10.12.0" + "@sentry/babel-plugin-component-annotate": "4.6.1", + "@sentry/browser": "10.26.0", + "@sentry/cli": "2.58.2", + "@sentry/core": "10.26.0", + "@sentry/react": "10.26.0", + "@sentry/types": "10.26.0" }, "bin": { "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" @@ -4260,12 +4268,12 @@ } }, "node_modules/@sentry/types": { - "version": "10.12.0", - "resolved": "https://mirrors.tencent.com/npm/@sentry/types/-/types-10.12.0.tgz", - "integrity": "sha512-sKGj3l3V8ZKISh2Tu88bHfnm5ztkRtSLdmpZ6TmCeJdSM9pV+RRd6CMJ0RnSEXmYHselPNUod521t2NQFd4W1w==", + "version": "10.26.0", + "resolved": "https://mirrors.tencent.com/npm/@sentry/types/-/types-10.26.0.tgz", + "integrity": "sha512-mDpG7lnOJppbk9iKrnuvkuiCTbh3aBAlUK4NZxZNLOSI0SeefYXHRAcri89BqWZ/MT98sQLU+Hf+rlwrwq38/A==", "license": "MIT", "dependencies": { - "@sentry/core": "10.12.0" + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" @@ -4408,9 +4416,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://mirrors.tencent.com/npm/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "version": "4.17.21", + "resolved": "https://mirrors.tencent.com/npm/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", "license": "MIT" }, "node_modules/@types/node": { @@ -5404,9 +5412,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "54.0.6", - "resolved": "https://mirrors.tencent.com/npm/babel-preset-expo/-/babel-preset-expo-54.0.6.tgz", - "integrity": "sha512-GxJfwnuOPQJbzDe5WASJZdNQiukLw7i9z+Lh6JQWkUHXsShHyQrqgiKE55MD/KaP9VqJ70yZm7bYqOu8zwcWqQ==", + "version": "54.0.7", + "resolved": "https://mirrors.tencent.com/npm/babel-preset-expo/-/babel-preset-expo-54.0.7.tgz", + "integrity": "sha512-JENWk0bvxW4I1ftveO8GRtX2t2TH6N4Z0TPvIHxroZ/4SswUfyNsUNbbP7Fm4erj3ar/JHGri5kTZ+s3xdjHZw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -5504,9 +5512,10 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.16", - "resolved": "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", - "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "version": "2.8.31", + "resolved": "https://mirrors.tencent.com/npm/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } @@ -5527,6 +5536,7 @@ "version": "8.4.2", "resolved": "https://mirrors.tencent.com/npm/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -5597,9 +5607,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.3", - "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.26.3.tgz", - "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "version": "4.28.0", + "resolved": "https://mirrors.tencent.com/npm/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "funding": [ { "type": "opencollective", @@ -5614,13 +5624,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.9", - "caniuse-lite": "^1.0.30001746", - "electron-to-chromium": "^1.5.227", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -5752,9 +5761,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001750", - "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", - "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", + "version": "1.0.30001757", + "resolved": "https://mirrors.tencent.com/npm/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", "funding": [ { "type": "opencollective", @@ -5943,6 +5952,7 @@ "version": "1.0.4", "resolved": "https://mirrors.tencent.com/npm/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", "engines": { "node": ">=0.8" } @@ -5992,6 +6002,7 @@ "version": "7.2.0", "resolved": "https://mirrors.tencent.com/npm/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", "engines": { "node": ">= 10" } @@ -6030,6 +6041,7 @@ "version": "2.6.9", "resolved": "https://mirrors.tencent.com/npm/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -6037,14 +6049,12 @@ "node_modules/compression/node_modules/ms": { "version": "2.0.0", "resolved": "https://mirrors.tencent.com/npm/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/compression/node_modules/negotiator": { "version": "0.6.4", "resolved": "https://mirrors.tencent.com/npm/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6092,12 +6102,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.46.0", - "resolved": "https://mirrors.tencent.com/npm/core-js-compat/-/core-js-compat-3.46.0.tgz", - "integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==", + "version": "3.47.0", + "resolved": "https://mirrors.tencent.com/npm/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", "license": "MIT", "dependencies": { - "browserslist": "^4.26.3" + "browserslist": "^4.28.0" }, "funding": { "type": "opencollective", @@ -6283,10 +6293,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.18", - "resolved": "https://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.18.tgz", - "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", - "license": "MIT" + "version": "1.11.19", + "resolved": "https://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" }, "node_modules/debug": { "version": "4.4.1", @@ -6307,7 +6316,7 @@ }, "node_modules/decode-uri-component": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "resolved": "https://mirrors.tencent.com/npm/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "license": "MIT", "engines": { @@ -6318,6 +6327,7 @@ "version": "0.6.0", "resolved": "https://mirrors.tencent.com/npm/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -6342,6 +6352,7 @@ "version": "1.0.4", "resolved": "https://mirrors.tencent.com/npm/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", "dependencies": { "clone": "^1.0.2" }, @@ -6370,6 +6381,7 @@ "version": "2.0.0", "resolved": "https://mirrors.tencent.com/npm/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", "engines": { "node": ">=8" } @@ -6558,9 +6570,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.234", - "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", - "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "version": "1.5.260", + "resolved": "https://mirrors.tencent.com/npm/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", + "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7246,29 +7258,29 @@ "license": "MIT" }, "node_modules/expo": { - "version": "54.0.21", - "resolved": "https://mirrors.tencent.com/npm/expo/-/expo-54.0.21.tgz", - "integrity": "sha512-I3kzMNW/43a71pt6hT0Zebd2zAPIMMeucUDDEdfUKYrzzTRwISZfVAv0dp8GWKHHDjZsy+FjE4RQCMdyKmiDeQ==", + "version": "54.0.25", + "resolved": "https://mirrors.tencent.com/npm/expo/-/expo-54.0.25.tgz", + "integrity": "sha512-+iSeBJfHRHzNPnHMZceEXhSGw4t5bNqFyd/5xMUoGfM+39rO7F72wxiLRpBKj0M6+0GQtMaEs+eTbcCrO7XyJQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "54.0.14", + "@expo/cli": "54.0.16", "@expo/config": "~12.0.10", "@expo/config-plugins": "~54.0.2", "@expo/devtools": "0.1.7", - "@expo/fingerprint": "0.15.2", + "@expo/fingerprint": "0.15.3", "@expo/metro": "~54.1.0", - "@expo/metro-config": "54.0.8", + "@expo/metro-config": "54.0.9", "@expo/vector-icons": "^15.0.3", "@ungap/structured-clone": "^1.3.0", - "babel-preset-expo": "~54.0.6", - "expo-asset": "~12.0.9", + "babel-preset-expo": "~54.0.7", + "expo-asset": "~12.0.10", "expo-constants": "~18.0.10", - "expo-file-system": "~19.0.17", + "expo-file-system": "~19.0.19", "expo-font": "~14.0.9", "expo-keep-awake": "~15.0.7", - "expo-modules-autolinking": "3.0.19", - "expo-modules-core": "3.0.23", + "expo-modules-autolinking": "3.0.22", + "expo-modules-core": "3.0.26", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-without-unicode": "8.0.0-3" @@ -7317,13 +7329,13 @@ } }, "node_modules/expo-asset": { - "version": "12.0.9", - "resolved": "https://mirrors.tencent.com/npm/expo-asset/-/expo-asset-12.0.9.tgz", - "integrity": "sha512-vrdRoyhGhBmd0nJcssTSk1Ypx3Mbn/eXaaBCQVkL0MJ8IOZpAObAjfD5CTy8+8RofcHEQdh3wwZVCs7crvfOeg==", + "version": "12.0.10", + "resolved": "https://mirrors.tencent.com/npm/expo-asset/-/expo-asset-12.0.10.tgz", + "integrity": "sha512-pZyeJkoDsALh4gpCQDzTA/UCLaPH/1rjQNGubmLn/uDM27S4iYJb/YWw4+CNZOtd5bCUOhDPg5DtGQnydNFSXg==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.8.7", - "expo-constants": "~18.0.9" + "expo-constants": "~18.0.10" }, "peerDependencies": { "expo": "*", @@ -7355,9 +7367,9 @@ } }, "node_modules/expo-camera": { - "version": "17.0.8", - "resolved": "https://mirrors.tencent.com/npm/expo-camera/-/expo-camera-17.0.8.tgz", - "integrity": "sha512-BIGvS+3myaYqMtk2VXWgdcOMrewH+55BttmaYqq9tv9+o5w+RAbH9wlJSt0gdaswikiyzoWT7mOnLDleYClXmw==", + "version": "17.0.9", + "resolved": "https://mirrors.tencent.com/npm/expo-camera/-/expo-camera-17.0.9.tgz", + "integrity": "sha512-KgticPGurqEsaPBIwbG0T6mzAVnqZasDdM/6OoJt5zPh6tWB09+th6cBF1WafIBMPy8AWbfyUQSqQXqOrNJClg==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -7374,6 +7386,17 @@ } } }, + "node_modules/expo-clipboard": { + "version": "8.0.7", + "resolved": "https://mirrors.tencent.com/npm/expo-clipboard/-/expo-clipboard-8.0.7.tgz", + "integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-constants": { "version": "18.0.10", "resolved": "https://mirrors.tencent.com/npm/expo-constants/-/expo-constants-18.0.10.tgz", @@ -7389,9 +7412,9 @@ } }, "node_modules/expo-file-system": { - "version": "19.0.17", - "resolved": "https://mirrors.tencent.com/npm/expo-file-system/-/expo-file-system-19.0.17.tgz", - "integrity": "sha512-WwaS01SUFrxBnExn87pg0sCTJjZpf2KAOzfImG0o8yhkU7fbYpihpl/oocXBEsNbj58a8hVt1Y4CVV5c1tzu/g==", + "version": "19.0.19", + "resolved": "https://mirrors.tencent.com/npm/expo-file-system/-/expo-file-system-19.0.19.tgz", + "integrity": "sha512-OrpOV4fEBFMFv+jy7PnENpPbsWoBmqWGidSwh1Ai52PLl6JIInYGfZTc6kqyPNGtFTwm7Y9mSWnE8g+dtLxu7g==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -7413,9 +7436,9 @@ } }, "node_modules/expo-glass-effect": { - "version": "0.1.5", - "resolved": "https://mirrors.tencent.com/npm/expo-glass-effect/-/expo-glass-effect-0.1.5.tgz", - "integrity": "sha512-DnleHtTpWET89I4GQsr4PDDvesKFA4miX+xV+1+ko5Z6SHkanBHD8gAOnvf07wTanzm4e8dAf0/abu7JmZlZIA==", + "version": "0.1.7", + "resolved": "https://mirrors.tencent.com/npm/expo-glass-effect/-/expo-glass-effect-0.1.7.tgz", + "integrity": "sha512-DxminueyL6TWoC9A3omka57XzpxUXLEGpDi/tnlvYwPSihB6lvGp2my+0k97lUKsBHbJg29weMCEQxNa/AyRHA==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -7492,12 +7515,12 @@ } }, "node_modules/expo-linking": { - "version": "8.0.8", - "resolved": "https://mirrors.tencent.com/npm/expo-linking/-/expo-linking-8.0.8.tgz", - "integrity": "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg==", + "version": "8.0.9", + "resolved": "https://mirrors.tencent.com/npm/expo-linking/-/expo-linking-8.0.9.tgz", + "integrity": "sha512-a0UHhlVyfwIbn8b1PSFPoFiIDJeps2iEq109hVH3CHd0CMKuRxFfNio9Axe2BjXhiJCYWR4OV1iIyzY/GjiVkQ==", "license": "MIT", "dependencies": { - "expo-constants": "~18.0.8", + "expo-constants": "~18.0.10", "invariant": "^2.2.4" }, "peerDependencies": { @@ -7529,15 +7552,14 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "3.0.19", - "resolved": "https://mirrors.tencent.com/npm/expo-modules-autolinking/-/expo-modules-autolinking-3.0.19.tgz", - "integrity": "sha512-tSMYGnfZmAaN77X8iMLiaSgbCFnA7eh6s2ac09J2N2N0Rcf2RCE27jg0c0XenTMTWUcM4QvLhsNHof/WtlKqPw==", + "version": "3.0.22", + "resolved": "https://mirrors.tencent.com/npm/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz", + "integrity": "sha512-Ej4SsZAnUUVFmbn6SoBso8K308mRKg8xgapdhP7v7IaSgfbexUoqxoiV31949HQQXuzmgvpkXCfp6Ex+mDW0EQ==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", - "glob": "^10.4.2", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, @@ -7546,9 +7568,9 @@ } }, "node_modules/expo-modules-core": { - "version": "3.0.23", - "resolved": "https://mirrors.tencent.com/npm/expo-modules-core/-/expo-modules-core-3.0.23.tgz", - "integrity": "sha512-NYHi5LK/cdIyOjK9ZQAgfDPCOqER26cIbU3gzsce7YdnsmlNFR0qMfWOj9zAmaFBviC2kCkCOmitwk4357Td3Q==", + "version": "3.0.26", + "resolved": "https://mirrors.tencent.com/npm/expo-modules-core/-/expo-modules-core-3.0.26.tgz", + "integrity": "sha512-WWjficXz32VmQ+xDoO+c0+jwDME0n/47wONrJkRvtm32H9W8n3MXkOMGemDl95HyPKYsaYKhjFGUOVOxIF3hcQ==", "license": "MIT", "dependencies": { "invariant": "^2.2.4" @@ -7593,9 +7615,9 @@ } }, "node_modules/expo-router": { - "version": "6.0.14", - "resolved": "https://mirrors.tencent.com/npm/expo-router/-/expo-router-6.0.14.tgz", - "integrity": "sha512-vizLO4SgnMEL+PPs2dXr+etEOuksjue7yUQBCtfCEdqoDkQlB0r35zI7rS34Wt53sxKWSlM2p+038qQEpxtiFw==", + "version": "6.0.15", + "resolved": "https://mirrors.tencent.com/npm/expo-router/-/expo-router-6.0.15.tgz", + "integrity": "sha512-PAettvLifQzb6hibCmBqxbR9UljlH61GvDRLyarGxs/tG9OpMXCoZHZo8gGCO24K1/6cchBKBcjvQ0PRrKwPew==", "license": "MIT", "dependencies": { "@expo/metro-runtime": "^6.1.2", @@ -7608,7 +7630,7 @@ "client-only": "^0.0.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "expo-server": "^1.0.3", + "expo-server": "^1.0.4", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", @@ -7628,7 +7650,7 @@ "@testing-library/react-native": ">= 12.0.0", "expo": "*", "expo-constants": "^18.0.10", - "expo-linking": "^8.0.8", + "expo-linking": "^8.0.9", "react": "*", "react-dom": "*", "react-native": "*", @@ -7846,21 +7868,21 @@ } }, "node_modules/expo-server": { - "version": "1.0.3", - "resolved": "https://mirrors.tencent.com/npm/expo-server/-/expo-server-1.0.3.tgz", - "integrity": "sha512-SOwdzM/BFAL+vTFlUDJG6ljhyk6TyTl+LRK3ubGmN+Pf18ENRqKj37U8krc5vH926sAsB3IFcE8kJEYf4dG7PA==", + "version": "1.0.4", + "resolved": "https://mirrors.tencent.com/npm/expo-server/-/expo-server-1.0.4.tgz", + "integrity": "sha512-IN06r3oPxFh3plSXdvBL7dx0x6k+0/g0bgxJlNISs6qL5Z+gyPuWS750dpTzOeu37KyBG0RcyO9cXUKzjYgd4A==", "license": "MIT", "engines": { "node": ">=20.16.0" } }, "node_modules/expo-splash-screen": { - "version": "31.0.10", - "resolved": "https://mirrors.tencent.com/npm/expo-splash-screen/-/expo-splash-screen-31.0.10.tgz", - "integrity": "sha512-i6g9IK798mae4yvflstQ1HkgahIJ6exzTCTw4vEdxV0J2SwiW3Tj+CwRjf0te7Zsb+7dDQhBTmGZwdv00VER2A==", + "version": "31.0.11", + "resolved": "https://mirrors.tencent.com/npm/expo-splash-screen/-/expo-splash-screen-31.0.11.tgz", + "integrity": "sha512-D7MQflYn/PAN3+fACSyxHO4oxZMBezllbgFdVY8roAS1gXpCy8SS6LrGHTD0VpOPEp3X4Gn7evTnXSI9nFoI5Q==", "license": "MIT", "dependencies": { - "@expo/prebuild-config": "^54.0.3" + "@expo/prebuild-config": "^54.0.6" }, "peerDependencies": { "expo": "*" @@ -7950,9 +7972,9 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "54.0.14", - "resolved": "https://mirrors.tencent.com/npm/@expo/cli/-/cli-54.0.14.tgz", - "integrity": "sha512-M7QW/GHx1FJg+CGgChGKerYXmCGWDskJ8S6w+8m49IBZ41CMDeWRH5snQkFoGCttF8WnzhGiX+nu69AFnEuDHQ==", + "version": "54.0.16", + "resolved": "https://mirrors.tencent.com/npm/@expo/cli/-/cli-54.0.16.tgz", + "integrity": "sha512-hY/OdRaJMs5WsVPuVSZ+RLH3VObJmL/pv5CGCHEZHN2PxZjSZSdctyKV8UcFBXTF0yIKNAJ9XLs1dlNYXHh4Cw==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.8", @@ -7963,9 +7985,9 @@ "@expo/env": "~2.0.7", "@expo/image-utils": "^0.8.7", "@expo/json-file": "^10.0.7", - "@expo/mcp-tunnel": "~0.0.7", + "@expo/mcp-tunnel": "~0.1.0", "@expo/metro": "~54.1.0", - "@expo/metro-config": "~54.0.8", + "@expo/metro-config": "~54.0.9", "@expo/osascript": "^2.3.7", "@expo/package-manager": "^1.9.8", "@expo/plist": "^0.4.7", @@ -7988,7 +8010,7 @@ "connect": "^3.7.0", "debug": "^4.3.4", "env-editor": "^0.4.1", - "expo-server": "^1.0.3", + "expo-server": "^1.0.4", "freeport-async": "^2.0.0", "getenv": "^2.0.0", "glob": "^10.4.2", @@ -8064,6 +8086,7 @@ "version": "7.7.3", "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8253,7 +8276,7 @@ }, "node_modules/filter-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "resolved": "https://mirrors.tencent.com/npm/filter-obj/-/filter-obj-1.1.0.tgz", "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", "license": "MIT", "engines": { @@ -8837,7 +8860,8 @@ "node_modules/hosted-git-info/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://mirrors.tencent.com/npm/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/html-parse-stringify": { "version": "3.0.1", @@ -9064,9 +9088,9 @@ } }, "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "version": "11.0.0", + "resolved": "https://mirrors.tencent.com/npm/immer/-/immer-11.0.0.tgz", + "integrity": "sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -10146,6 +10170,7 @@ "cpu": [ "arm64" ], + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -10265,6 +10290,7 @@ "cpu": [ "x64" ], + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -10304,6 +10330,7 @@ "cpu": [ "arm64" ], + "license": "MPL-2.0", "optional": true, "os": [ "win32" @@ -10395,6 +10422,7 @@ "version": "2.2.0", "resolved": "https://mirrors.tencent.com/npm/log-symbols/-/log-symbols-2.2.0.tgz", "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", "dependencies": { "chalk": "^2.0.1" }, @@ -10438,7 +10466,8 @@ "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.3", "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/log-symbols/node_modules/escape-string-regexp": { "version": "1.0.5", @@ -10453,6 +10482,7 @@ "version": "3.0.0", "resolved": "https://mirrors.tencent.com/npm/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -10461,6 +10491,7 @@ "version": "5.5.0", "resolved": "https://mirrors.tencent.com/npm/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -10954,7 +10985,6 @@ "version": "1.54.0", "resolved": "https://mirrors.tencent.com/npm/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -10984,6 +11014,7 @@ "version": "1.2.0", "resolved": "https://mirrors.tencent.com/npm/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", "engines": { "node": ">=4" } @@ -11136,9 +11167,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://mirrors.tencent.com/npm/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://mirrors.tencent.com/npm/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -11151,9 +11182,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.23", - "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.23.tgz", - "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "version": "2.0.27", + "resolved": "https://mirrors.tencent.com/npm/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/normalize-path": { @@ -11184,6 +11215,7 @@ "version": "7.7.3", "resolved": "https://mirrors.tencent.com/npm/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -11391,6 +11423,7 @@ "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/onetime/-/onetime-2.0.1.tgz", "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" }, @@ -11453,6 +11486,7 @@ "version": "4.1.1", "resolved": "https://mirrors.tencent.com/npm/ansi-regex/-/ansi-regex-4.1.1.tgz", "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", "engines": { "node": ">=6" } @@ -11493,7 +11527,8 @@ "node_modules/ora/node_modules/color-name": { "version": "1.1.3", "resolved": "https://mirrors.tencent.com/npm/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/ora/node_modules/escape-string-regexp": { "version": "1.0.5", @@ -11508,6 +11543,7 @@ "version": "3.0.0", "resolved": "https://mirrors.tencent.com/npm/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { "node": ">=4" } @@ -11527,6 +11563,7 @@ "version": "5.5.0", "resolved": "https://mirrors.tencent.com/npm/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -11929,7 +11966,7 @@ }, "node_modules/query-string": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "resolved": "https://mirrors.tencent.com/npm/query-string/-/query-string-7.1.3.tgz", "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", "license": "MIT", "dependencies": { @@ -11994,6 +12031,7 @@ "version": "1.2.8", "resolved": "https://mirrors.tencent.com/npm/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -12008,6 +12046,7 @@ "version": "2.0.1", "resolved": "https://mirrors.tencent.com/npm/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12260,17 +12299,17 @@ } }, "node_modules/react-native-purchases": { - "version": "9.5.4", - "resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.5.4.tgz", - "integrity": "sha512-N2S5GvV6gkcl9O+1XzzH10AjPMYie2aWoHi2QTN2fG4tulGSHhTYIYhX0QjkJzN0eX2ACRgPB4BUxcbh1/DERQ==", + "version": "9.6.7", + "resolved": "https://mirrors.tencent.com/npm/react-native-purchases/-/react-native-purchases-9.6.7.tgz", + "integrity": "sha512-gbb2l0oKkmxKo7Zk11BgOWeGnixiMU+Kig/8QYS78ka7S5rc8aR38RzygtXmPEfxhwRT7w4uzCVV49bab0RPjg==", "license": "MIT", "workspaces": [ "examples/purchaseTesterTypescript", "react-native-purchases-ui" ], "dependencies": { - "@revenuecat/purchases-js-hybrid-mappings": "17.10.0", - "@revenuecat/purchases-typescript-internal": "17.10.0" + "@revenuecat/purchases-js-hybrid-mappings": "17.19.1", + "@revenuecat/purchases-typescript-internal": "17.19.1" }, "peerDependencies": { "react": ">= 16.6.3", @@ -12284,9 +12323,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "4.1.3", - "resolved": "https://mirrors.tencent.com/npm/react-native-reanimated/-/react-native-reanimated-4.1.3.tgz", - "integrity": "sha512-GP8wsi1u3nqvC1fMab/m8gfFwFyldawElCcUSBJQgfrXeLmsPPUOpDw44lbLeCpcwUuLa05WTVePdTEwCLTUZg==", + "version": "4.1.5", + "resolved": "https://mirrors.tencent.com/npm/react-native-reanimated/-/react-native-reanimated-4.1.5.tgz", + "integrity": "sha512-UA6VUbxwhRjEw2gSNrvhkusUq3upfD3Cv+AnB07V+kC8kpvwRVI+ivwY95ePbWNFkFpP+Y2Sdw1WHpHWEV+P2Q==", "license": "MIT", "dependencies": { "react-native-is-edge-to-edge": "^1.2.1", @@ -12706,14 +12745,14 @@ }, "node_modules/regenerate": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "resolved": "https://mirrors.tencent.com/npm/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://mirrors.tencent.com/npm/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -12750,17 +12789,17 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://mirrors.tencent.com/npm/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -12768,34 +12807,22 @@ }, "node_modules/regjsgen": { "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "resolved": "https://mirrors.tencent.com/npm/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://mirrors.tencent.com/npm/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12831,6 +12858,7 @@ "version": "1.7.1", "resolved": "https://mirrors.tencent.com/npm/resolve/-/resolve-1.7.1.tgz", "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "license": "MIT", "dependencies": { "path-parse": "^1.0.5" } @@ -13543,7 +13571,7 @@ }, "node_modules/split-on-first": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "resolved": "https://mirrors.tencent.com/npm/split-on-first/-/split-on-first-1.1.0.tgz", "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", "license": "MIT", "engines": { @@ -13636,7 +13664,7 @@ }, "node_modules/strict-uri-encode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "resolved": "https://mirrors.tencent.com/npm/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", "license": "MIT", "engines": { @@ -13949,10 +13977,10 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://mirrors.tencent.com/npm/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", - "license": "ISC", + "version": "7.5.2", + "resolved": "https://mirrors.tencent.com/npm/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -13968,6 +13996,7 @@ "version": "5.0.0", "resolved": "https://mirrors.tencent.com/npm/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -14403,7 +14432,7 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "resolved": "https://mirrors.tencent.com/npm/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "license": "MIT", "engines": { @@ -14412,7 +14441,7 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "resolved": "https://mirrors.tencent.com/npm/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "license": "MIT", "dependencies": { @@ -14424,18 +14453,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://mirrors.tencent.com/npm/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://mirrors.tencent.com/npm/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -14513,9 +14542,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://mirrors.tencent.com/npm/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "funding": [ { "type": "opencollective", @@ -14530,7 +14559,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -14672,7 +14700,6 @@ "version": "1.1.2", "resolved": "https://mirrors.tencent.com/npm/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -15385,12 +15412,12 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://mirrors.tencent.com/npm/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.0", + "resolved": "https://mirrors.tencent.com/npm/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/package.json b/package.json index cb01ed8..e2ab6d9 100644 --- a/package.json +++ b/package.json @@ -11,27 +11,28 @@ "dependencies": { "@expo/metro-runtime": "~6.1.2", "@expo/ui": "~0.2.0-beta.7", - "@expo/vector-icons": "^15.0.2", + "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "^2.2.0", - "@react-native-community/datetimepicker": "8.4.4", + "@react-native-community/datetimepicker": "8.5.1", "@react-native-masked-view/masked-view": "^0.3.2", - "@react-native-picker/picker": "2.11.1", + "@react-native-picker/picker": "2.11.4", "@react-native-voice/voice": "^3.2.4", - "@react-navigation/bottom-tabs": "^7.4.0", - "@react-navigation/elements": "^2.6.4", - "@react-navigation/native": "^7.1.8", - "@reduxjs/toolkit": "^2.9.0", - "@sentry/react-native": "~7.2.0", - "@types/lodash": "^4.17.20", - "dayjs": "^1.11.18", - "expo": "54.0.21", + "@react-navigation/bottom-tabs": "^7.8.6", + "@react-navigation/elements": "^2.8.3", + "@react-navigation/native": "^7.1.21", + "@reduxjs/toolkit": "^2.11.0", + "@sentry/react-native": "~7.7.0", + "@types/lodash": "^4.17.21", + "dayjs": "^1.11.19", + "expo": "54.0.25", "expo-apple-authentication": "~8.0.7", "expo-background-task": "~1.0.8", "expo-blur": "~15.0.7", - "expo-camera": "~17.0.8", - "expo-constants": "~18.0.9", + "expo-clipboard": "~8.0.7", + "expo-camera": "~17.0.9", + "expo-constants": "~18.0.10", "expo-font": "~14.0.9", - "expo-glass-effect": "~0.1.5", + "expo-glass-effect": "~0.1.7", "expo-haptics": "~15.0.7", "expo-image": "~3.0.10", "expo-image-picker": "~17.0.8", @@ -41,8 +42,8 @@ "expo-media-library": "^18.2.0", "expo-notifications": "~0.32.12", "expo-quick-actions": "^6.0.0", - "expo-router": "~6.0.14", - "expo-splash-screen": "~31.0.10", + "expo-router": "~6.0.15", + "expo-splash-screen": "~31.0.11", "expo-sqlite": "^16.0.8", "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", @@ -84,4 +85,4 @@ "typescript": "~5.9.2" }, "private": true -} +} \ No newline at end of file diff --git a/services/api.ts b/services/api.ts index 4a6f02c..c96d857 100644 --- a/services/api.ts +++ b/services/api.ts @@ -82,14 +82,38 @@ async function handle401Unauthorized() { } } +// Token 缓存:内存中保存一份,避免每次都读取 AsyncStorage let inMemoryToken: string | null = null; +/** + * 设置认证 token + * 同时更新内存缓存和持久化存储 + */ export async function setAuthToken(token: string | null): Promise { inMemoryToken = token; + + // 同步更新 AsyncStorage + if (token) { + await AsyncStorage.setItem(STORAGE_KEYS.authToken, token); + } else { + await AsyncStorage.removeItem(STORAGE_KEYS.authToken); + } } -export function getAuthToken(): Promise { - return AsyncStorage.getItem(STORAGE_KEYS.authToken); +/** + * 获取认证 token + * 优先使用内存缓存,若无则从 AsyncStorage 读取并缓存 + */ +export async function getAuthToken(): Promise { + // 如果内存中有,直接返回 + if (inMemoryToken !== null) { + return inMemoryToken; + } + + // 否则从 AsyncStorage 读取并缓存到内存 + const token = await AsyncStorage.getItem(STORAGE_KEYS.authToken); + inMemoryToken = token; + return token; } export type ApiRequestOptions = { diff --git a/services/challengesApi.ts b/services/challengesApi.ts index a6b7f3b..8e48098 100644 --- a/services/challengesApi.ts +++ b/services/challengesApi.ts @@ -2,11 +2,24 @@ import { api } from './api'; export type ChallengeStatus = 'upcoming' | 'ongoing' | 'expired'; +export enum ChallengeSource { + SYSTEM = 'system', + CUSTOM = 'custom', +} + +export enum ChallengeState { + DRAFT = 'draft', + ACTIVE = 'active', + ARCHIVED = 'archived', +} + export type ChallengeProgressDto = { completed: number; target: number; remaining: number checkedInToday: boolean; + lastProgressAt?: string; + last_progress_at?: string; }; export type RankingItemDto = { @@ -38,7 +51,7 @@ export type ChallengeListItemDto = { durationLabel: string; requirementLabel: string; unit?: string; - status: ChallengeStatus; + status?: ChallengeStatus; participantsCount: number; rankingDescription?: string; highlightTitle: string; @@ -50,12 +63,23 @@ export type ChallengeListItemDto = { endAt?: string; minimumCheckInDays: number; // 最小打卡天数 type: ChallengeType; + shareCode?: string | null; + source?: ChallengeSource; + creatorId?: string | null; + isCreator?: boolean; + isPublic?: boolean; + maxParticipants?: number | null; + challengeState?: ChallengeState; + progressUnit?: string; + targetValue?: number; + summary?: string | null; }; export type ChallengeDetailDto = ChallengeListItemDto & { - summary?: string; - rankings: RankingItemDto[]; + summary?: string | null; + rankings?: RankingItemDto[]; userRank?: number; + challengeState?: ChallengeState; }; export type ChallengeRankingsDto = { @@ -65,6 +89,31 @@ export type ChallengeRankingsDto = { items: RankingItemDto[]; }; +export type ChallengeListResponse = { + items: ChallengeListItemDto[]; + total: number; + page: number; + pageSize: number; +}; + +export type CreateCustomChallengePayload = { + title: string; + type: ChallengeType; + image?: string | null; + startAt: number; + endAt: number; + targetValue: number; + minimumCheckInDays: number; + durationLabel: string; + requirementLabel: string; + summary?: string | null; + progressUnit?: string; + periodLabel?: string | null; + rankingDescription?: string | null; + isPublic?: boolean; + maxParticipants?: number | null; +}; + export async function listChallenges(): Promise { return api.get('/challenges'); } @@ -101,3 +150,43 @@ export async function getChallengeRankings( const url = `/challenges/${encodeURIComponent(id)}/rankings${query ? `?${query}` : ''}`; return api.get(url); } + +export async function listMyCustomChallenges( + params?: { page?: number; pageSize?: number; state?: ChallengeState } +): Promise { + const searchParams = new URLSearchParams(); + if (params?.page) { + searchParams.append('page', String(params.page)); + } + if (params?.pageSize) { + searchParams.append('pageSize', String(params.pageSize)); + } + if (params?.state) { + searchParams.append('state', params.state); + } + const query = searchParams.toString(); + const url = `/challenges/my/created${query ? `?${query}` : ''}`; + return api.get(url); +} + +export async function createCustomChallenge( + payload: CreateCustomChallengePayload +): Promise { + return api.post('/challenges/custom', payload); +} + +export async function joinChallengeByCode(shareCode: string): Promise { + return api.post('/challenges/join-by-code', { shareCode }); +} + +export async function getChallengeByShareCode(shareCode: string): Promise { + return api.get(`/challenges/share/${encodeURIComponent(shareCode)}`); +} + +export async function regenerateChallengeShareCode( + id: string +): Promise<{ shareCode: string }> { + return api.post<{ shareCode: string }>( + `/challenges/custom/${encodeURIComponent(id)}/regenerate-code` + ); +} diff --git a/store/challengesSlice.ts b/store/challengesSlice.ts index aabba71..f7a24a4 100644 --- a/store/challengesSlice.ts +++ b/store/challengesSlice.ts @@ -1,13 +1,21 @@ +import dayjs from 'dayjs'; + import { appStoreReviewService } from '@/services/appStoreReview'; import { type ChallengeDetailDto, type ChallengeListItemDto, type ChallengeProgressDto, + ChallengeSource, + ChallengeState, type ChallengeStatus, + type CreateCustomChallengePayload, type RankingItemDto, + createCustomChallenge, + getChallengeByShareCode, getChallengeDetail, getChallengeRankings, joinChallenge as joinChallengeApi, + joinChallengeByCode as joinChallengeByCodeApi, leaveChallenge as leaveChallengeApi, listChallenges, reportChallengeProgress as reportChallengeProgressApi, @@ -21,9 +29,9 @@ export type ChallengeProgress = ChallengeProgressDto; export type RankingItem = RankingItemDto; export type ChallengeSummary = ChallengeListItemDto; export type ChallengeDetail = ChallengeDetailDto; -export type { ChallengeStatus }; +export type { ChallengeSource, ChallengeState, ChallengeStatus }; export type ChallengeEntity = ChallengeSummary & { - summary?: string; + summary?: string | null; rankings?: RankingItem[]; userRank?: number; }; @@ -38,7 +46,7 @@ type ChallengeRankingList = { type ChallengesState = { entities: Record; - order: string[]; + orderedIds: string[]; listStatus: AsyncStatus; listError?: string; detailStatus: Record; @@ -53,11 +61,15 @@ type ChallengesState = { rankingStatus: Record; rankingLoadMoreStatus: Record; rankingError: Record; + createStatus: AsyncStatus; + createError?: string; + joinByCodeStatus: AsyncStatus; + joinByCodeError?: string; }; const initialState: ChallengesState = { entities: {}, - order: [], + orderedIds: [], listStatus: 'idle', listError: undefined, detailStatus: {}, @@ -72,6 +84,10 @@ const initialState: ChallengesState = { rankingStatus: {}, rankingLoadMoreStatus: {}, rankingError: {}, + createStatus: 'idle', + createError: undefined, + joinByCodeStatus: 'idle', + joinByCodeError: undefined, }; const toErrorMessage = (error: unknown): string => { @@ -168,10 +184,41 @@ export const fetchChallengeRankings = createAsyncThunk< } }); +export const createCustomChallengeThunk = createAsyncThunk< + ChallengeDetail, + CreateCustomChallengePayload, + { rejectValue: string } +>('challenges/createCustom', async (payload, { rejectWithValue }) => { + try { + return await createCustomChallenge(payload); + } catch (error) { + return rejectWithValue(toErrorMessage(error)); + } +}); + +export const joinChallengeByCode = createAsyncThunk< + { challenge: ChallengeDetail; progress: ChallengeProgress }, + string, + { rejectValue: string } +>('challenges/joinByCode', async (shareCode, { rejectWithValue }) => { + try { + const progress = await joinChallengeByCodeApi(shareCode); + const challenge = await getChallengeByShareCode(shareCode); + return { challenge: { ...challenge, progress }, progress }; + } catch (error) { + return rejectWithValue(toErrorMessage(error)); + } +}); + const challengesSlice = createSlice({ name: 'challenges', initialState, - reducers: {}, + reducers: { + resetJoinByCodeState: (state) => { + state.joinByCodeStatus = 'idle'; + state.joinByCodeError = undefined; + }, + }, extraReducers: (builder) => { builder .addCase(fetchChallenges.pending, (state) => { @@ -181,18 +228,15 @@ const challengesSlice = createSlice({ .addCase(fetchChallenges.fulfilled, (state, action) => { state.listStatus = 'succeeded'; state.listError = undefined; - const ids = new Set(); + const incomingIds = new Set(); action.payload.forEach((challenge) => { - ids.add(challenge.id); + incomingIds.add(challenge.id); + const source = challenge.source ?? ChallengeSource.SYSTEM; const existing = state.entities[challenge.id]; - if (existing) { - Object.assign(existing, challenge); - } else { - state.entities[challenge.id] = { ...challenge }; - } + state.entities[challenge.id] = { ...(existing ?? {}), ...challenge, source }; }); Object.keys(state.entities).forEach((id) => { - if (!ids.has(id)) { + if (!incomingIds.has(id) && !state.entities[id]?.isJoined) { delete state.entities[id]; delete state.detailStatus[id]; delete state.detailError[id]; @@ -204,7 +248,7 @@ const challengesSlice = createSlice({ delete state.progressError[id]; } }); - state.order = action.payload.map((item) => item.id); + state.orderedIds = action.payload.map((item) => item.id); }) .addCase(fetchChallenges.rejected, (state, action) => { state.listStatus = 'failed'; @@ -220,11 +264,8 @@ const challengesSlice = createSlice({ state.detailStatus[detail.id] = 'succeeded'; state.detailError[detail.id] = undefined; const existing = state.entities[detail.id]; - if (existing) { - Object.assign(existing, detail); - } else { - state.entities[detail.id] = { ...detail }; - } + const source = detail.source ?? existing?.source ?? ChallengeSource.SYSTEM; + state.entities[detail.id] = { ...(existing ?? {}), ...detail, source }; }) .addCase(fetchChallengeDetail.rejected, (state, action) => { const id = action.meta.arg; @@ -333,9 +374,50 @@ const challengesSlice = createSlice({ state.rankingError[id] = message; } }); + + builder + .addCase(createCustomChallengeThunk.pending, (state) => { + state.createStatus = 'loading'; + state.createError = undefined; + }) + .addCase(createCustomChallengeThunk.fulfilled, (state, action) => { + state.createStatus = 'succeeded'; + state.createError = undefined; + const challenge = action.payload; + const existing = state.entities[challenge.id]; + const source = ChallengeSource.CUSTOM; + state.entities[challenge.id] = { ...(existing ?? {}), ...challenge, source }; + state.orderedIds = [challenge.id, ...state.orderedIds.filter((id) => id !== challenge.id)]; + }) + .addCase(createCustomChallengeThunk.rejected, (state, action) => { + state.createStatus = 'failed'; + state.createError = action.payload ?? toErrorMessage(action.error); + }); + + builder + .addCase(joinChallengeByCode.pending, (state) => { + state.joinByCodeStatus = 'loading'; + state.joinByCodeError = undefined; + }) + .addCase(joinChallengeByCode.fulfilled, (state, action) => { + state.joinByCodeStatus = 'succeeded'; + state.joinByCodeError = undefined; + const { challenge, progress } = action.payload; + const existing = state.entities[challenge.id]; + const source = challenge.source ?? existing?.source ?? ChallengeSource.SYSTEM; + const merged = { ...(existing ?? {}), ...challenge, progress, isJoined: true, source }; + state.entities[challenge.id] = merged as ChallengeEntity; + state.orderedIds = [challenge.id, ...state.orderedIds.filter((id) => id !== challenge.id)]; + }) + .addCase(joinChallengeByCode.rejected, (state, action) => { + state.joinByCodeStatus = 'failed'; + state.joinByCodeError = action.payload ?? toErrorMessage(action.error); + }); }, }); +export const { resetJoinByCodeState } = challengesSlice.actions; + export default challengesSlice.reducer; const selectChallengesState = (state: RootState) => state.challenges; @@ -355,20 +437,30 @@ export const selectChallengeEntities = createSelector( (state) => state.entities ); -export const selectChallengeOrder = createSelector( +const selectChallengeOrder = createSelector( [selectChallengesState], - (state) => state.order + (state) => state.orderedIds ); export const selectChallengeList = createSelector( [selectChallengeEntities, selectChallengeOrder], - (entities, order) => order.map((id) => entities[id]).filter(Boolean) as ChallengeEntity[] + (entities, orderedIds) => orderedIds.map((id) => entities[id]).filter(Boolean) as ChallengeEntity[] +); + +export const selectCustomChallengeList = createSelector( + [selectChallengeList], + (list) => list.filter((challenge) => challenge.source === ChallengeSource.CUSTOM) +); + +export const selectOfficialChallengeList = createSelector( + [selectChallengeList], + (list) => list.filter((challenge) => challenge.source !== ChallengeSource.CUSTOM) ); const formatNumberWithSeparator = (value: number): string => value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -const formatMonthDay = (input: string | undefined): string | undefined => { +const formatMonthDay = (input: string | number | undefined): string | undefined => { if (!input) return undefined; const date = new Date(input); if (Number.isNaN(date.getTime())) return undefined; @@ -384,6 +476,26 @@ const buildDateRangeLabel = (challenge: ChallengeEntity): string => { return challenge.periodLabel ?? challenge.durationLabel; }; +const deriveStatus = (challenge: ChallengeEntity): ChallengeStatus => { + if (challenge.status) return challenge.status; + if (challenge.challengeState === ChallengeState.ARCHIVED) { + return 'expired'; + } + const now = dayjs(); + const start = challenge.startAt ? dayjs(challenge.startAt) : null; + const end = challenge.endAt ? dayjs(challenge.endAt) : null; + if (start?.isValid() && start.isAfter(now)) { + return 'upcoming'; + } + if (end?.isValid() && end.isBefore(now)) { + return 'expired'; + } + return 'ongoing'; +}; + +const FALLBACK_CHALLENGE_IMAGE = + 'https://images.unsplash.com/photo-1506126613408-eca07ce68773?auto=format&fit=crop&w=1000&q=80'; + export type ChallengeCardViewModel = { id: string; title: string; @@ -392,7 +504,7 @@ export type ChallengeCardViewModel = { participantsLabel: string; status: ChallengeStatus; isJoined: boolean; - endAt?: string; + endAt?: string | number; periodLabel?: string; durationLabel: string; requirementLabel: string; @@ -401,27 +513,109 @@ export type ChallengeCardViewModel = { ctaLabel: string; progress?: ChallengeProgress; avatars: string[]; + source?: ChallengeSource; + shareCode?: string | null; + challengeState?: ChallengeState; + progressUnit?: string; + targetValue?: number; + isCreator?: boolean; }; export const selectChallengeCards = createSelector([selectChallengeList], (challenges) => - challenges.map((challenge) => ({ - id: challenge.id, - title: challenge.title, - image: challenge.image, - dateRange: buildDateRangeLabel(challenge), - participantsLabel: `${formatNumberWithSeparator(challenge.participantsCount)} 人参与`, - status: challenge.status, - isJoined: challenge.isJoined, - endAt: challenge.endAt, - periodLabel: challenge.periodLabel, - durationLabel: challenge.durationLabel, - requirementLabel: challenge.requirementLabel, - highlightTitle: challenge.highlightTitle, - highlightSubtitle: challenge.highlightSubtitle, - ctaLabel: challenge.ctaLabel, - progress: challenge.progress, - avatars: [], - })) + challenges.map((challenge) => { + const participants = + typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0; + return { + id: challenge.id, + title: challenge.title, + image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE, + dateRange: buildDateRangeLabel(challenge), + participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`, + status: deriveStatus(challenge), + isJoined: challenge.isJoined, + endAt: challenge.endAt, + periodLabel: challenge.periodLabel, + durationLabel: challenge.durationLabel, + requirementLabel: challenge.requirementLabel, + highlightTitle: challenge.highlightTitle, + highlightSubtitle: challenge.highlightSubtitle, + ctaLabel: challenge.ctaLabel, + progress: challenge.progress, + avatars: [], + source: challenge.source, + shareCode: challenge.shareCode ?? null, + challengeState: challenge.challengeState, + progressUnit: challenge.progressUnit, + targetValue: challenge.targetValue, + isCreator: challenge.isCreator, + }; + }) +); + +export const selectCustomChallengeCards = createSelector( + [selectCustomChallengeList], + (challenges) => + challenges.map((challenge) => { + const participants = + typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0; + return { + id: challenge.id, + title: challenge.title, + image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE, + dateRange: buildDateRangeLabel(challenge), + participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`, + status: deriveStatus(challenge), + isJoined: challenge.isJoined, + endAt: challenge.endAt, + periodLabel: challenge.periodLabel, + durationLabel: challenge.durationLabel, + requirementLabel: challenge.requirementLabel, + highlightTitle: challenge.highlightTitle, + highlightSubtitle: challenge.highlightSubtitle, + ctaLabel: challenge.ctaLabel, + progress: challenge.progress, + avatars: [], + source: challenge.source ?? ChallengeSource.CUSTOM, + shareCode: challenge.shareCode ?? null, + challengeState: challenge.challengeState, + progressUnit: challenge.progressUnit, + targetValue: challenge.targetValue, + isCreator: challenge.isCreator, + }; + }) +); + +export const selectOfficialChallengeCards = createSelector( + [selectOfficialChallengeList], + (challenges) => + challenges.map((challenge) => { + const participants = + typeof challenge.participantsCount === 'number' ? challenge.participantsCount : 0; + return { + id: challenge.id, + title: challenge.title, + image: challenge.image ?? FALLBACK_CHALLENGE_IMAGE, + dateRange: buildDateRangeLabel(challenge), + participantsLabel: `${formatNumberWithSeparator(participants)} 人参与`, + status: deriveStatus(challenge), + isJoined: challenge.isJoined, + endAt: challenge.endAt, + periodLabel: challenge.periodLabel, + durationLabel: challenge.durationLabel, + requirementLabel: challenge.requirementLabel, + highlightTitle: challenge.highlightTitle, + highlightSubtitle: challenge.highlightSubtitle, + ctaLabel: challenge.ctaLabel, + progress: challenge.progress, + avatars: [], + source: challenge.source ?? ChallengeSource.SYSTEM, + shareCode: challenge.shareCode ?? null, + challengeState: challenge.challengeState, + progressUnit: challenge.progressUnit, + targetValue: challenge.targetValue, + isCreator: challenge.isCreator, + }; + }) ); export const selectChallengeById = (id: string) => @@ -462,3 +656,23 @@ export const selectChallengeRankingLoadMoreStatus = (id: string) => export const selectChallengeRankingError = (id: string) => createSelector([selectChallengesState], (state) => state.rankingError[id]); + +export const selectCreateChallengeStatus = createSelector( + [selectChallengesState], + (state) => state.createStatus +); + +export const selectCreateChallengeError = createSelector( + [selectChallengesState], + (state) => state.createError +); + +export const selectJoinByCodeStatus = createSelector( + [selectChallengesState], + (state) => state.joinByCodeStatus +); + +export const selectJoinByCodeError = createSelector( + [selectChallengesState], + (state) => state.joinByCodeError +); diff --git a/store/userSlice.ts b/store/userSlice.ts index 9986111..b305a5a 100644 --- a/store/userSlice.ts +++ b/store/userSlice.ts @@ -5,65 +5,51 @@ import AsyncStorage from '@/utils/kvStore'; import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; import dayjs from 'dayjs'; -// 预加载的用户数据存储 -let preloadedUserData: { - token: string | null; - profile: UserProfile; - privacyAgreed: boolean; - onboardingCompleted: boolean; -} | null = null; - -// 预加载用户数据的函数 -export async function preloadUserData() { +/** + * 同步加载用户数据(在 Redux store 初始化时立即执行) + * 使用 getItemSync 确保数据在 store 创建前就已加载 + */ +function loadUserDataSync() { try { - const [profileStr, privacyAgreedStr, token, onboardingCompletedStr] = await Promise.all([ - AsyncStorage.getItem(STORAGE_KEYS.userProfile), - AsyncStorage.getItem(STORAGE_KEYS.privacyAgreed), - AsyncStorage.getItem(STORAGE_KEYS.authToken), - AsyncStorage.getItem(STORAGE_KEYS.onboardingCompleted), - ]); + const profileStr = AsyncStorage.getItemSync(STORAGE_KEYS.userProfile); + const token = AsyncStorage.getItemSync(STORAGE_KEYS.authToken); + const onboardingCompletedStr = AsyncStorage.getItemSync(STORAGE_KEYS.onboardingCompleted); let profile: UserProfile = { memberNumber: 0 }; + if (profileStr) { try { profile = JSON.parse(profileStr) as UserProfile; } catch { - profile = { - memberNumber: 0 - }; + profile = { memberNumber: 0 }; } } - const privacyAgreed = privacyAgreedStr === 'true'; const onboardingCompleted = onboardingCompletedStr === 'true'; - // 如果有 token,需要设置到 API 客户端 + // 如果有 token,需要异步设置到 API 客户端(但不阻塞初始化) if (token) { - await setAuthToken(token); + setAuthToken(token).catch(err => { + console.error('设置 auth token 失败:', err); + }); } - preloadedUserData = { token, profile, privacyAgreed, onboardingCompleted }; - return preloadedUserData; + return { token, profile, onboardingCompleted }; } catch (error) { - console.error('预加载用户数据失败:', error); - preloadedUserData = { + console.error('同步加载用户数据失败:', error); + return { token: null, - profile: { - memberNumber: 0 - }, - privacyAgreed: false, + profile: { memberNumber: 0 }, onboardingCompleted: false }; - return preloadedUserData; } } -// 获取预加载的用户数据 -function getPreloadedUserData() { - return preloadedUserData || { token: null, profile: {}, privacyAgreed: false, onboardingCompleted: false }; -} +// 在模块加载时立即同步加载用户数据 +const preloadedUserData = loadUserDataSync(); + export type Gender = 'male' | 'female' | ''; @@ -120,22 +106,23 @@ export type UserState = { export const DEFAULT_MEMBER_NAME = '朋友'; const getInitialState = (): UserState => { - const preloaded = getPreloadedUserData(); + // 使用模块加载时同步加载的数据 + console.log('初始化 Redux state,使用预加载数据:', preloadedUserData); + return { - token: preloaded.token, + token: preloadedUserData.token, profile: { name: DEFAULT_MEMBER_NAME, isVip: false, freeUsageCount: 3, - memberNumber: 0, maxUsageCount: 5, - ...preloaded.profile, // 合并预加载的用户资料 + ...preloadedUserData.profile, // 合并预加载的用户资料(包含 memberNumber) }, loading: false, error: null, weightHistory: [], activityHistory: [], - onboardingCompleted: preloaded.onboardingCompleted, // 引导完成状态 + onboardingCompleted: preloadedUserData.onboardingCompleted, // 引导完成状态 }; }; @@ -198,8 +185,11 @@ export const login = createAsyncThunk( if (!token) throw new Error('登录响应缺少 token'); + // 先持久化到本地存储 await AsyncStorage.setItem(STORAGE_KEYS.authToken, token); await AsyncStorage.setItem(STORAGE_KEYS.userProfile, JSON.stringify(profile ?? {})); + + // 再设置到 API 客户端(内部会同步更新 AsyncStorage) await setAuthToken(token); return { token, profile } as { token: string; profile: UserProfile }; @@ -222,12 +212,15 @@ export const setOnboardingCompleted = createAsyncThunk('user/setOnboardingComple }); export const logout = createAsyncThunk('user/logout', async () => { + // 先清除 API 客户端的 token(内部会清除 AsyncStorage) + await setAuthToken(null); + + // 再清除其他本地存储数据 await Promise.all([ - AsyncStorage.removeItem(STORAGE_KEYS.authToken), AsyncStorage.removeItem(STORAGE_KEYS.userProfile), AsyncStorage.removeItem(STORAGE_KEYS.privacyAgreed), ]); - await setAuthToken(null); + return true; });