From c3d46308010b23a2a2d989848a9df503e59a7b81 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 12 Aug 2025 19:21:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=92=8C=E6=B3=95=E5=BE=8B=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增登录页面,支持 Apple 登录和游客登录功能 - 添加用户协议和隐私政策页面,用户需同意后才能登录 - 更新首页逻辑,首次进入时自动跳转到登录页面 - 修改个人信息页面,移除单位选择功能,统一使用 kg 和 cm - 更新依赖,添加 expo-apple-authentication 库以支持 Apple 登录 - 更新布局以适应新功能的展示和交互 --- app/(tabs)/explore.tsx | 8 +- app/(tabs)/index.tsx | 11 +- app/(tabs)/personal.tsx | 12 +- app/_layout.tsx | 3 + app/auth/login.tsx | 225 ++++++++++++++++++ app/legal/privacy-policy.tsx | 15 ++ app/legal/user-agreement.tsx | 15 ++ app/profile/edit.tsx | 98 ++------ ios/Podfile.lock | 6 + ios/digitalpilates.xcodeproj/project.pbxproj | 5 +- .../digitalpilates.entitlements | 18 +- package-lock.json | 10 + package.json | 3 +- 13 files changed, 326 insertions(+), 103 deletions(-) create mode 100644 app/auth/login.tsx create mode 100644 app/legal/privacy-policy.tsx create mode 100644 app/legal/user-agreement.tsx diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index fcf4f2c..2fcb4a3 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -187,7 +187,13 @@ export default function ExploreScreen() { ) : ( ——/2000 )} - + diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5b2c1d7..4a2c2e6 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -5,7 +5,7 @@ import { ThemedView } from '@/components/ThemedView'; import { WorkoutCard } from '@/components/WorkoutCard'; import { getChineseGreeting } from '@/utils/date'; import { useRouter } from 'expo-router'; -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { SafeAreaView, ScrollView, StyleSheet, View } from 'react-native'; const workoutData = [ @@ -24,6 +24,15 @@ const workoutData = [ export default function HomeScreen() { const router = useRouter(); + const hasOpenedLoginRef = useRef(false); + + useEffect(() => { + // 仅在本次会话首次进入首页时打开登录页,可返回关闭 + if (!hasOpenedLoginRef.current) { + hasOpenedLoginRef.current = true; + router.push('/auth/login'); + } + }, [router]); return ( diff --git a/app/(tabs)/personal.tsx b/app/(tabs)/personal.tsx index ff0d3f9..6dc6d04 100644 --- a/app/(tabs)/personal.tsx +++ b/app/(tabs)/personal.tsx @@ -31,8 +31,6 @@ export default function PersonalScreen() { const colorScheme = useColorScheme(); const colors = Colors[colorScheme ?? 'light']; - type WeightUnit = 'kg' | 'lb'; - type HeightUnit = 'cm' | 'ft'; type UserProfile = { fullName?: string; email?: string; @@ -40,7 +38,7 @@ export default function PersonalScreen() { age?: string; weightKg?: number; heightCm?: number; - unitPref?: { weight: WeightUnit; height: HeightUnit }; + avatarUri?: string | null; }; const [profile, setProfile] = useState({}); @@ -78,16 +76,12 @@ export default function PersonalScreen() { const formatHeight = () => { if (profile.heightCm == null) return '--'; - const unit = profile.unitPref?.height ?? 'cm'; - if (unit === 'cm') return `${Math.round(profile.heightCm)}cm`; - return `${round(profile.heightCm / 30.48, 1)}ft`; + return `${Math.round(profile.heightCm)}cm`; }; const formatWeight = () => { if (profile.weightKg == null) return '--'; - const unit = profile.unitPref?.weight ?? 'kg'; - if (unit === 'kg') return `${round(profile.weightKg, 1)}kg`; - return `${round(profile.weightKg * 2.2046226218, 1)}lb`; + return `${round(profile.weightKg, 1)}kg`; }; const formatAge = () => (profile.age ? `${profile.age}岁` : '--'); diff --git a/app/_layout.tsx b/app/_layout.tsx index cb19f69..3c4c7de 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -24,6 +24,9 @@ export default function RootLayout() { + + + diff --git a/app/auth/login.tsx b/app/auth/login.tsx new file mode 100644 index 0000000..a2fac81 --- /dev/null +++ b/app/auth/login.tsx @@ -0,0 +1,225 @@ +import { Ionicons } from '@expo/vector-icons'; +import * as AppleAuthentication from 'expo-apple-authentication'; +import { useRouter } from 'expo-router'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Alert, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +import { ThemedText } from '@/components/ThemedText'; +import { ThemedView } from '@/components/ThemedView'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; + +export default function LoginScreen() { + const router = useRouter(); + const scheme = (useColorScheme() ?? 'light') as 'light' | 'dark'; + const color = Colors[scheme]; + + const [hasAgreed, setHasAgreed] = useState(false); + const [appleAvailable, setAppleAvailable] = useState(false); + const [loading, setLoading] = useState(false); + + useEffect(() => { + AppleAuthentication.isAvailableAsync().then(setAppleAvailable).catch(() => setAppleAvailable(false)); + }, []); + + const guardAgreement = useCallback((action: () => void) => { + if (!hasAgreed) { + Alert.alert('请先阅读并同意', '勾选“我已阅读并同意用户协议与隐私政策”后才可继续登录'); + return; + } + action(); + }, [hasAgreed]); + + const onAppleLogin = useCallback(async () => { + if (!appleAvailable) return; + try { + setLoading(true); + const credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], + }); + // TODO: 将 credential 发送到后端换取应用会话。这里先直接返回上一页。 + router.back(); + } catch (err: any) { + if (err?.code === 'ERR_CANCELED') return; + Alert.alert('登录失败', '请稍后再试'); + } finally { + setLoading(false); + } + }, [appleAvailable, router]); + + const onGuestLogin = useCallback(() => { + // TODO: 标记为游客身份,可在此写入本地状态/上报统计 + router.back(); + }, [router]); + + const disabledStyle = useMemo(() => ({ opacity: hasAgreed ? 1 : 0.5 }), [hasAgreed]); + + return ( + + + {/* 自定义头部,与其它页面风格一致 */} + + router.back()} style={styles.backButton}> + + + 登录 + + + + + + Digital Pilates + 欢迎登录 + + + {/* Apple 登录 */} + {appleAvailable && ( + guardAgreement(onAppleLogin)} + disabled={!hasAgreed || loading} + style={({ pressed }) => [ + styles.appleButton, + { backgroundColor: '#000000' }, + disabledStyle, + pressed && { transform: [{ scale: 0.98 }] }, + ]} + > + + 使用 Apple 登录 + + )} + + {/* 游客登录(弱化样式) */} + guardAgreement(onGuestLogin)} + disabled={!hasAgreed || loading} + style={({ pressed }) => [ + styles.guestButton, + { borderColor: color.border, backgroundColor: color.surface }, + disabledStyle, + pressed && { transform: [{ scale: 0.98 }] }, + ]} + > + + 以游客身份继续 + + + {/* 协议勾选 */} + + setHasAgreed((v) => !v)} style={styles.checkboxWrap} accessibilityRole="checkbox" accessibilityState={{ checked: hasAgreed }}> + + {hasAgreed && } + + + 我已阅读并同意 + router.push('/legal/privacy-policy')}> + 《隐私政策》 + + + router.push('/legal/user-agreement')}> + 《用户协议》 + + + + {/* 占位底部间距 */} + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { flex: 1 }, + container: { flex: 1 }, + content: { + flexGrow: 1, + paddingHorizontal: 24, + justifyContent: 'center', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingTop: 4, + paddingBottom: 8, + }, + backButton: { width: 32, height: 32, alignItems: 'center', justifyContent: 'center' }, + headerTitle: { fontSize: 18, fontWeight: '700' }, + headerWrap: { + marginBottom: 36, + }, + title: { + fontSize: 32, + fontWeight: '800', + letterSpacing: 0.5, + }, + subtitle: { + marginTop: 8, + fontSize: 14, + fontWeight: '500', + }, + appleButton: { + height: 56, + borderRadius: 28, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + marginBottom: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.15, + shadowRadius: 12, + elevation: 2, + }, + appleText: { + fontSize: 16, + color: '#FFFFFF', + fontWeight: '600', + }, + guestButton: { + height: 52, + borderRadius: 26, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + borderWidth: 1, + marginTop: 6, + }, + guestText: { + fontSize: 15, + fontWeight: '500', + }, + agreementRow: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + marginTop: 24, + }, + checkboxWrap: { marginRight: 8 }, + checkbox: { + width: 18, + height: 18, + borderRadius: 5, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + }, + agreementText: { fontSize: 12 }, + link: { fontSize: 12, fontWeight: '600' }, + footerHint: { marginTop: 24 }, + hintText: { fontSize: 12 }, +}); + + diff --git a/app/legal/privacy-policy.tsx b/app/legal/privacy-policy.tsx new file mode 100644 index 0000000..98c2305 --- /dev/null +++ b/app/legal/privacy-policy.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { ScrollView, Text } from 'react-native'; + +export default function PrivacyPolicy() { + return ( + + 隐私政策(示例) + + 这是占位文案,用于展示隐私政策内容。请替换为正式的隐私政策文本。 + + + ); +} + + diff --git a/app/legal/user-agreement.tsx b/app/legal/user-agreement.tsx new file mode 100644 index 0000000..34245fa --- /dev/null +++ b/app/legal/user-agreement.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { ScrollView, Text } from 'react-native'; + +export default function UserAgreement() { + return ( + + 用户协议(示例) + + 这是占位文案,用于说明用户协议。请在此替换为你们的正式协议内容。 + + + ); +} + + diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx index ff4a5cd..8dc9e10 100644 --- a/app/profile/edit.tsx +++ b/app/profile/edit.tsx @@ -4,7 +4,7 @@ import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as ImagePicker from 'expo-image-picker'; import { router } from 'expo-router'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, Image, @@ -32,10 +32,6 @@ interface UserProfile { weightKg?: number; // kg heightCm?: number; // cm avatarUri?: string | null; - unitPref?: { - weight: WeightUnit; - height: HeightUnit; - }; } const STORAGE_KEY = '@user_profile'; @@ -52,26 +48,12 @@ export default function EditProfileScreen() { weightKg: undefined, heightCm: undefined, avatarUri: null, - unitPref: { weight: 'kg', height: 'cm' }, }); const [weightInput, setWeightInput] = useState(''); const [heightInput, setHeightInput] = useState(''); - // 将存储的公制值转换为当前选择单位显示 - const displayedWeight = useMemo(() => { - if (profile.weightKg == null || isNaN(profile.weightKg)) return ''; - return profile.unitPref?.weight === 'kg' - ? String(round(profile.weightKg, 1)) - : String(round(kgToLb(profile.weightKg), 1)); - }, [profile.weightKg, profile.unitPref?.weight]); - - const displayedHeight = useMemo(() => { - if (profile.heightCm == null || isNaN(profile.heightCm)) return ''; - return profile.unitPref?.height === 'cm' - ? String(Math.round(profile.heightCm)) - : String(round(cmToFt(profile.heightCm), 1)); - }, [profile.heightCm, profile.unitPref?.height]); + // 输入框字符串 useEffect(() => { (async () => { @@ -89,7 +71,6 @@ export default function EditProfileScreen() { weightKg: undefined, heightCm: undefined, avatarUri: null, - unitPref: { weight: 'kg', height: 'cm' }, }; if (fromOnboarding) { @@ -109,8 +90,8 @@ export default function EditProfileScreen() { } catch { } } setProfile(next); - setWeightInput(next.weightKg != null ? (next.unitPref?.weight === 'kg' ? String(round(next.weightKg, 1)) : String(round(kgToLb(next.weightKg), 1))) : ''); - setHeightInput(next.heightCm != null ? (next.unitPref?.height === 'cm' ? String(Math.round(next.heightCm)) : String(round(cmToFt(next.heightCm), 1))) : ''); + setWeightInput(next.weightKg != null ? String(round(next.weightKg, 1)) : ''); + setHeightInput(next.heightCm != null ? String(Math.round(next.heightCm)) : ''); } catch (e) { console.warn('读取资料失败', e); } @@ -124,17 +105,17 @@ export default function EditProfileScreen() { try { const next: UserProfile = { ...profile }; - // 将当前输入反向同步为公制 + // 将当前输入同步为公制(固定 kg/cm) const w = parseFloat(weightInput); if (!isNaN(w)) { - next.weightKg = profile.unitPref?.weight === 'kg' ? w : lbToKg(w); + next.weightKg = w; } else { next.weightKg = undefined; } const h = parseFloat(heightInput); if (!isNaN(h)) { - next.heightCm = profile.unitPref?.height === 'cm' ? h : ftToCm(h); + next.heightCm = h; } else { next.heightCm = undefined; } @@ -147,27 +128,7 @@ export default function EditProfileScreen() { } }; - const toggleWeightUnit = (unit: WeightUnit) => { - if (unit === profile.unitPref?.weight) return; - const current = parseFloat(weightInput); - let nextValueStr = weightInput; - if (!isNaN(current)) { - nextValueStr = unit === 'kg' ? String(round(lbToKg(current), 1)) : String(round(kgToLb(current), 1)); - } - setProfile((p) => ({ ...p, unitPref: { ...(p.unitPref || { weight: 'kg', height: 'cm' }), weight: unit } })); - setWeightInput(nextValueStr); - }; - - const toggleHeightUnit = (unit: HeightUnit) => { - if (unit === profile.unitPref?.height) return; - const current = parseFloat(heightInput); - let nextValueStr = heightInput; - if (!isNaN(current)) { - nextValueStr = unit === 'cm' ? String(Math.round(ftToCm(current))) : String(round(cmToFt(current), 1)); - } - setProfile((p) => ({ ...p, unitPref: { ...(p.unitPref || { weight: 'kg', height: 'cm' }), height: unit } })); - setHeightInput(nextValueStr); - }; + // 不再需要单位切换 const pickAvatarFromLibrary = async () => { try { @@ -235,44 +196,36 @@ export default function EditProfileScreen() { {!!profile.fullName && } - {/* 体重 */} + {/* 体重(kg) */} + kg - toggleWeightUnit(key as WeightUnit)} - /> - {/* 身高 */} + {/* 身高(cm) */} + cm - toggleHeightUnit(key as HeightUnit)} - /> {/* 性别 */} @@ -323,29 +276,10 @@ function FieldLabel({ text }: { text: string }) { ); } -function SegmentedTwo(props: { - options: { key: string; label: string }[]; - activeKey: string; - onChange: (key: string) => void; -}) { - const { options, activeKey, onChange } = props; - return ( - - {options.map((opt) => ( - onChange(opt.key)} - activeOpacity={0.8} - > - {opt.label} - - ))} - - ); -} +// 单位切换组件已移除(固定 kg/cm) // 工具函数 +// 转换函数不再使用,保留 round function kgToLb(kg: number) { return kg * 2.2046226218; } function lbToKg(lb: number) { return lb / 2.2046226218; } function cmToFt(cm: number) { return cm / 30.48; } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7939600..bbcad37 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -33,6 +33,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - ExpoAppleAuthentication (6.4.2): + - ExpoModulesCore - ExpoAsset (11.1.7): - ExpoModulesCore - ExpoBlur (14.1.5): @@ -2156,6 +2158,7 @@ DEPENDENCIES: - EXConstants (from `../node_modules/expo-constants/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) - Expo (from `../node_modules/expo`) + - ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoBlur (from `../node_modules/expo-blur/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) @@ -2273,6 +2276,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-image-loader/ios" Expo: :path: "../node_modules/expo" + ExpoAppleAuthentication: + :path: "../node_modules/expo-apple-authentication/ios" ExpoAsset: :path: "../node_modules/expo-asset/ios" ExpoBlur: @@ -2465,6 +2470,7 @@ SPEC CHECKSUMS: EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee Expo: a40d525c930dd1c8a158e082756ee071955baccb + ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5 ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9 ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 diff --git a/ios/digitalpilates.xcodeproj/project.pbxproj b/ios/digitalpilates.xcodeproj/project.pbxproj index 29cccf3..1795b1c 100644 --- a/ios/digitalpilates.xcodeproj/project.pbxproj +++ b/ios/digitalpilates.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = digitalpilates/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = digitalpilates/Info.plist; sourceTree = ""; }; 4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-digitalpilates.debug.xcconfig"; path = "Target Support Files/Pods-digitalpilates/Pods-digitalpilates.debug.xcconfig"; sourceTree = ""; }; - 7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 7EC44F9488C227087AA8DF97 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = digitalpilates/PrivacyInfo.xcprivacy; sourceTree = ""; }; 83D1B5F0EC906D7A2F599549 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-digitalpilates/ExpoModulesProvider.swift"; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = digitalpilates/SplashScreen.storyboard; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; @@ -73,7 +73,6 @@ 4D6B8E20DD8E5677F8B2EAA1 /* Pods-digitalpilates.debug.xcconfig */, EA6A757B2DE1747F7B3664B4 /* Pods-digitalpilates.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -340,6 +339,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 756WVXJ6MT; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", @@ -376,6 +376,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = digitalpilates/digitalpilates.entitlements; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 756WVXJ6MT; INFOPLIST_FILE = digitalpilates/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/ios/digitalpilates/digitalpilates.entitlements b/ios/digitalpilates/digitalpilates.entitlements index b37fe68..c41dab6 100644 --- a/ios/digitalpilates/digitalpilates.entitlements +++ b/ios/digitalpilates/digitalpilates.entitlements @@ -1,10 +1,14 @@ - - com.apple.developer.healthkit - - com.apple.developer.healthkit.access - - - \ No newline at end of file + + com.apple.developer.applesignin + + Default + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + + diff --git a/package-lock.json b/package-lock.json index f2a84e2..4af286c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@react-navigation/native": "^7.1.6", "dayjs": "^1.11.13", "expo": "~53.0.20", + "expo-apple-authentication": "6.4.2", "expo-blur": "~14.1.5", "expo-constants": "~17.1.7", "expo-font": "~13.3.2", @@ -6329,6 +6330,15 @@ } } }, + "node_modules/expo-apple-authentication": { + "version": "6.4.2", + "resolved": "https://mirrors.tencent.com/npm/expo-apple-authentication/-/expo-apple-authentication-6.4.2.tgz", + "integrity": "sha512-X4u1n3Ql1hOpztXHbKNq4I1l4+Ff82gC6RmEeW43Eht7VE6E8PrQBpYKw+JJv8osrCJt7R5O1PZwed6WLN5oig==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "11.1.7", "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.1.7.tgz", diff --git a/package.json b/package.json index c7f8056..e19747d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dayjs": "^1.11.13", "expo": "~53.0.20", "expo-blur": "~14.1.5", + "expo-apple-authentication": "6.4.2", "expo-constants": "~17.1.7", "expo-font": "~13.3.2", "expo-haptics": "~14.1.4", @@ -52,4 +53,4 @@ "typescript": "~5.8.3" }, "private": true -} +} \ No newline at end of file